Введение: Мифы и реальность безопасности Rust
Язык Rust часто называют "безопасным" и "надёжным", и это во многом справедливо. Благодаря системе владения (ownership) и строгому компилятору, Rust устраняет целые классы уязвимостей, характерных для C и C++ (например, переполнение буфера или висячие указатели). Однако это не означает, что Rust-приложения неуязвимы. На практике разработчики сталкиваются с ошибками логики, неправильным использованием unsafe и уязвимостями на уровне архитектуры.
В этой статье мы разберём топ-10 наиболее частых уязвимостей в Rust-приложениях. Вы узнаете, как они возникают, как их обнаружить и как защитить свой код. Материал будет полезен как новичкам, так и опытным разработчикам.
1. Ошибки в unsafe коде
Самая известная "ахиллесова пята" Rust — блоки unsafe. Они позволяют выполнять операции, которые компилятор не может проверить: разыменование сырых указателей, вызов внешних функций (FFI), изменение статических переменных. Если в unsafe допущена ошибка, вся безопасность Rust рушится.
Пример уязвимости
fn unsafe_deref(ptr: *const i32) -> i32 {
unsafe { *ptr } // Если ptr нулевой или указывает на освобождённую память — UB
}
Как защититься
- Минимизируйте использование
unsafe. Выносите его в небольшие, тщательно протестированные модули. - Используйте
#[deny(unsafe_code)]в крейтах, где это возможно. - Проверяйте указатели на валидность перед разыменованием.
2. Гонки данных (Data Races) в многопоточном коде
Rust гарантирует отсутствие гонок данных в безопасном коде благодаря правилам владения и заимствования. Однако при использовании unsafe или неправильной работе с Mutex/RwLock гонки возможны. Особенно опасны ситуации с Cell/RefCell в многопоточной среде.
Пример уязвимости
use std::cell::RefCell;
use std::thread;
fn main() {
let data = RefCell::new(0);
thread::spawn(move || {
*data.borrow_mut() += 1; // Паника: RefCell не Sync
});
}
Как защититься
- Используйте
Arc<Mutex<T>>илиArc<RwLock<T>>для разделяемых данных. - Избегайте
unsafeпри работе с потоками без крайней необходимости. - Применяйте статический анализатор
clippyиmiriдля обнаружения неопределённого поведения.
3. Утечки памяти (Memory Leaks)
Хотя Rust предотвращает утечки в большинстве случаев, они всё же возможны. Например, при создании циклических ссылок с Rc/RefCell или при забывании вызвать деструктор в unsafe коде. Утечки памяти в долгоживущих сервисах могут привести к исчерпанию ресурсов.
Пример уязвимости
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
next: Option<Rc<RefCell<Node>>>,
}
fn main() {
let a = Rc::new(RefCell::new(Node { next: None }));
let b = Rc::new(RefCell::new(Node { next: Some(a.clone()) }));
a.borrow_mut().next = Some(b); // Циклическая ссылка — утечка
}
Как защититься
- Используйте
Weakдля разрыва циклических ссылок. - Для долгоживущих приложений применяйте профилировщики памяти (например,
valgrindилиheaptrack). - В
unsafeкоде всегда проверяйте, что память освобождается корректно.
4. Неправильная обработка ошибок
Rust поощряет явную обработку ошибок через Result и Option. Однако разработчики часто игнорируют ошибки, используя .unwrap() или .expect(). В production это может привести к панике сервиса.
Пример уязвимости
fn read_file(path: &str) -> String {
std::fs::read_to_string(path).unwrap() // Паника, если файла нет
}
Как защититься
- Избегайте
.unwrap()в production коде. Используйтеmatchили?оператор. - Определите стратегию обработки ошибок: логирование, возврат пользователю, повторные попытки.
- Применяйте
thiserrorилиanyhowдля удобной работы с ошибками.
5. Уязвимости в зависимостях (Supply Chain)
Rust-экосистема (crates.io) насчитывает тысячи крейтов. Даже если ваш код безопасен, уязвимости в зависимостях могут скомпрометировать приложение. Особенно опасны крейты с unsafe или устаревшие версии.
Как защититься
- Регулярно обновляйте зависимости:
cargo update. - Используйте
cargo auditдля поиска известных уязвимостей. - Проверяйте код критических зависимостей вручную или через
cargo geiger(анализunsafe). - Внедрите политику минимальных привилегий: подключайте только нужные крейты.
6. Переполнение целых чисел (Integer Overflow)
В Rust переполнение целых чисел в debug-режиме вызывает панику, а в release-режиме выполняется по модулю (wrap). Это может привести к неожиданному поведению, особенно в финансовых или кри