Безопасность 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 умирает после