Найти тему
Один Rust не п...Rust

Безопасный код Rust

Оглавление

Для чего нужна данная статья? :


- изучить способы обеспечения безопасности кода:

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

"владения" (ownership) и "заимствования" (borrowing), которые позволяют строго управлять доступом к данным и предотвращать гонки данных и неопределенное поведение.

действия с данными, которые могли бы привести к утечкам памяти или другим проблемам безопасности.

ошибки и исключения, что помогает избегать ошибок безопасности.

управление памятью через систему владения и управление временем жизни ссылок (lifetimes), что позволяет избегать утечек памяти, гонок данных и ошибок при работе с указателями.

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

"unsafe" блоки, которые позволяют обойти строгие проверки безопасности, но при этом оставляют ответственность за безопасность на разработчике.

механизмы, такие как Safe Rust и Unsafe Rust, для управления безопасностью кода и защиты от атак на уровне памяти.

контроль доступа к системным ресурсам, таким как файлы, сеть и другие, что помогает предотвращать уязвимости на уровне системных вызовов.

Зачем Вам это уметь? :

1. Безопасность на уровне языка

  • Ownership (Владение) – исключает «use-after-free» и двойное освобождение памяти.
  • Borrowing (Заимствование) и Lifetimes (Времена жизни) – предотвращают гонки данных и висячие ссылки.
  • Мутируемость по умолчанию отключена – изменять данные можно только явно.
  • Безопасные абстракции – Rust компилятор запрещает небезопасные операции без unsafe.

// ownership.rs

use std::fmt::Display;

// Демонстрация владения (ownership)

struct SecureData {

value: String,

}

impl SecureData {

fn new(data: &str) -> Self {

SecureData { value: data.to_string() }

}

// Передача владения

fn consume(self) {

println!("Consumed data: {}", self.value);

}

}

// Функция с заимствованием и временем жизни

fn print_data<'a, T: Display>(data: &'a T) {

println!("{}", data);

}

fn ownership_demo() {

let data = SecureData::new("Secret");

print_data(&data.value); // Только заимствование

data.consume(); // Передача владения

// println!("{}", data.value); // Ошибка: value был перемещен

}

2. Избегание unsafe кода

  • Использовать unsafe только в исключительных случаях, проверять его вручную.
  • Минимизировать unsafe код и инкапсулировать его в безопасные API.
  • Использовать статические анализаторы (cargo-geiger, cargo-audit) для поиска потенциальных уязвимостей в unsafe коде.

// safe_wrapper.rs

use std::ptr;

// Безопасный API для работы с необработанными указателями

struct SafePointer {

ptr: *mut i32,

}

impl SafePointer {

fn new(val: i32) -> Self {

let boxed = Box::new(val);

SafePointer {

ptr: Box::into_raw(boxed),

}

}

fn get_value(&self) -> i32 {

unsafe { *self.ptr }

}

fn free(self) {

unsafe { drop(Box::from_raw(self.ptr)) };

}

}

fn safe_wrapper_demo() {

let safe_ptr = SafePointer::new(42);

println!("Safe value: {}", safe_ptr.get_value());

safe_ptr.free();

}

3. Работа с зависимостями

  • Использовать cargo audit для проверки зависимостей на известные уязвимости.
  • Регулярно обновлять зависимости (cargo update).
  • Использовать минимальный набор зависимостей, избегая потенциально уязвимых или ненужных пакетов.
  • Проверять код зависимостей перед их использованием.

4. Защита от атак

  • Безопасная обработка ошибок: избегать unwrap(), expect(), а использовать ? и Result.
  • Избегание утечек информации: не использовать dbg!(), println!() в релизных билдах, не логировать чувствительные данные.
  • Использование защищенных типов для паролей – например, secrecy::SecretString.
  • Защита от атак по времени – использовать защищенные алгоритмы сравнения (constant_time_eq).
  • Минимизация поверхностей атаки – скрывать внутренние API (pub(crate) вместо pub).

// security.rs

use secrecy::{Secret, ExposeSecret};

use subtle::ConstantTimeEq;

// Защита паролей

fn secure_password_check(input: &str, stored_hash: &Secret<String>) -> bool {

let input_hash = Secret::new(input.to_string()); // Пример, в реальном коде используйте Argon2

input_hash.expose_secret().as_bytes().ct_eq(stored_hash.expose_secret().as_bytes()).into()

}

5. Безопасность многопоточных программ

  • Использовать потокобезопасные примитивы (Arc<Mutex<T>>, RwLock, AtomicUsize).
  • Избегать unsafe при работе с потоками, использовать Send и Sync для гарантии безопасного использования.
  • Минимизировать разделяемое состояние.

// multithreading.rs

use std::sync::{Arc, Mutex};

use std::thread;

fn multithreading_demo() {

let counter = Arc::new(Mutex::new(0));

let mut handles = vec![];

for _ in 0..10 {

let counter = Arc::clone(&counter);

let handle = thread::spawn(move || {

let mut num = counter.lock().unwrap();

*num += 1;

});

handles.push(handle);

}

for handle in handles {

handle.join().unwrap();

}

println!("Final count: {}", *counter.lock().unwrap());

}

6. Безопасность веб-приложений и API

  • Sanitization и валидация входных данных – использовать строгую валидацию (validator, serde).
  • Защита от SQL-инъекций – применять подготовленные запросы (sqlx, diesel).
  • Безопасное хеширование паролей – использовать argon2, bcrypt, pbkdf2.
  • Защита от XSS и CSRF – использовать библиотеки вроде axum-extra::extract::CookieJar.

// web_security.rs

use axum::{routing::post, Router};

use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]

struct Login {

username: String,

password: String,

}

async fn login_handler(data: axum::Json<Login>) -> &'static str {

if data.username == "admin" && data.password == "securepass" {

"Login successful"

} else {

"Unauthorized"

}

}

fn web_server() -> Router {

Router::new().route("/login", post(login_handler))

}

7. Криптография и безопасность данных

  • Использовать проверенные криптографические библиотеки (ring, rustls, openssl).
  • Избегать собственных криптоалгоритмов.
  • Генерировать случайные данные с помощью rand::rngs::OsRng.

// crypto.rs

use rand::rngs::OsRng;

use ring::digest::{digest, SHA256};

fn hash_data(data: &str) -> String {

let hash = digest(&SHA256, data.as_bytes());

hex::encode(hash.as_ref())

}

8. Анализатор безопасности

  • cargo-audit – проверяет зависимости на уязвимости.
  • cargo-geiger – ищет unsafe код.
  • cargo-crev – проверяет надежность зависимостей.
  • clippy – анализирует код на ошибки и неэффективные конструкции.

9. Безопасность среды выполнения

  • Настройка Sandboxing (seccomp, bubblewrap, gVisor).
  • Минимизация привилегий – запускать сервисы от отдельного пользователя с ограниченными правами.
  • Использование SELinux, AppArmor – ограничение доступа к ресурсам ОС.

10. CI/CD и DevOps

  • Сборка в изолированной среде – использовать Docker или Nix.
  • Подпись бинарных файлов – защищает от подмены (cosign, sigstore).
  • Автоматизированное сканирование безопасности (Snyk, Trivy, GitHub Dependabot).

Как этого достичь ? :

Научиться управлять памятью, избегать утечек памяти и ситуаций, связанных с неопределенным поведением, таких как обращение к освобожденной памяти. Rust использует систему собственности и заимствования для обеспечения безопасности работы с памятью.

struct MyData {

value: String,

}
impl MyData {

// Метод для создания нового экземпляра MyData
fn new(value: &str) -> MyData {

MyData {

value: String::from(value),

}

}

// Метод для получения значения (заимствование неизменяемой ссылки)

fn get_value(&self) -> &String {

&self.value

}

}

fn main() {

// Создание нового экземпляра MyData

let data = MyData::new("Hello, Rust!");

// Заимствование неизменяемой ссылки на значение

let value_ref = data.get_value();

// Вывод значения на экран

println!("Value: {}", value_ref);

// В этой точке переменная `data` выходит из области видимости,

// поскольку заимствовали неизменяемую ссылку, а не значение,

// Rust не освобождает память и не происходит утечек.

// переменная `value_ref` также выходит из области видимости,

// и ничего не происходит, мы только заимствовали ссылку.

// Например получим изменяемую ссылку:

// let mut mutable_ref = data.value; // Ошибка компиляции!

}


Научиться создавать структуры данных, такие как
Rc и Arc, которые позволяют делить доступ к данным между несколькими потоками или владельцами, обеспечивая безопасность и предотвращая гонки.

use std::rc::Rc;

use std::sync::Arc;

use std::thread;

// Пример структуры данных, которую мы будем оборачивать в Rc или Arc
#[derive(Debug)]

struct SharedData {

value: i32,

}

impl SharedData {

fn new(value: i32) -> Self {

SharedData { value }

}

fn get_value(&self) -> i32 {

self.value

}

}

fn main() {
// Создаем Rc и Arc вокруг нашей структуры данных

let shared_data_rc = Rc::new(SharedData::new(42));

let shared_data_arc = Arc::new(SharedData::new(42));

// Клонируем Rc и Arc, чтобы увеличить счетчик ссылок

let rc_clone1 = Rc::clone(&shared_data_rc);

let rc_clone2 = Rc::clone(&shared_data_rc);

let arc_clone1 = Arc::clone(&shared_data_arc);

let arc_clone2 = Arc::clone(&shared_data_arc);

// потоки для демонстрации использования в многопоточной среде
let thread_rc = thread::spawn(move || {

println!("Thread with Rc: {:?}", rc_clone1);

println!("Thread with Rc: {:?}", rc_clone2);

});

let thread_arc = thread::spawn(move || {

println!("Thread with Arc: {:?}", arc_clone1);

println!("Thread with Arc: {:?}", arc_clone2);

});

// Дожидаемся завершения потоков

thread_rc.join().unwrap();

thread_arc.join().unwrap();

// В этой точке Rc автоматически освобождает память,

// так как счетчик ссылок становится равным нулю.

// Аналогично, Arc также автоматически управляет счетчиком ссылок,

// и освобождает память при необходимости.

}


Научиться создавать безопасную инициализацию при работе с многопоточностью, можно использовать паттерны и структуры данных, такие как
Mutex, RwLock и Once.

use std::sync::{Mutex, RwLock, Arc};

use std::thread;

struct SharedData {

value: i32,

}
impl SharedData {

fn new(value: i32) -> Self {

SharedData { value }

}

fn get_value(&self) -> i32 {

self.value

}

}

// безопасная инициализация через Once и Mutex

struct SharedDataContainer {

initialized: bool,

data: Option<SharedData>,

initialization_lock: Mutex<()>,

}

impl SharedDataContainer {

fn new() -> Self {

SharedDataContainer {

initialized: false,

data: None,

initialization_lock: Mutex::new(()),

}

}

fn initialize(&mut self, value: i32) {

// инициализация будет выполнена только один раз

static INIT: std::sync::Once = std::sync::Once::new();

INIT.call_once(|| {

// Захватываем мьютекс для безопасной инициализации данных

let _lock = self.initialization_lock.lock().unwrap();

// данные инициализированы другим потоком
if !self.initialized {

self.data = Some(SharedData::new(value));

self.initialized = true;

}

});

}

fn get_data(&self) -> Option<&SharedData> {

self.data.as_ref()

}
}

fn main() {

// Создаем общий контейнер для данных

let shared_data_container = Arc::new(SharedDataContainer::new());

// Клонируем Arc для использования в разных потоках

let thread_container1 = Arc::clone(&shared_data_container);

let thread_container2 = Arc::clone(&shared_data_container);

// Создаем потоки для демонстрации многопоточной инициализации

let thread1 = thread::spawn(move || {

thread_container1.initialize(42);

println!("Thread 1: {:?}", thread_container1.get_data().unwrap());

});

let thread2 = thread::spawn(move || {

thread_container2.initialize(99);

println!("Thread 2: {:?}", thread_container2.get_data().unwrap());

});
// Дожидаемся завершения потоков

thread1.join().unwrap();

thread2.join().unwrap();

// В этой точке данные уже прошли инициализацию.

println!("Main thread: {:?}", shared_data_container.get_data().unwrap());

}

Научиться создавать безопасный ввод-вывод, включая типажи Read и Write, и обработку ошибок с помощью Result. Это позволяет избегать уязвимостей, связанных с некорректными операциями ввода-вывода.

use std::fs::File;

use std::io::{self, Read, Write};

fn read_file_content(file_path: &str) -> Result<String, io::Error> {

// Открываем файл в режиме только для чтения

let mut file = File::open(file_path)?;

// Читаем содержимое файла в строку

let mut content = String::new();

file.read_to_string(&mut content)?;

Ok(content)

}

fn write_to_file(file_path: &str, content: &str) -> Result<(), io::Error> {

// Открываем файл в режиме записи

let mut file = File::create(file_path)?;

// Записываем содержимое в файл

file.write_all(content.as_bytes())?;

Ok(())

}

fn main() {

// Пример чтения из файла и записи в другой файл

let input_file_path = "input.txt";

let output_file_path = "output.txt";

// Пишем данные в файл для примера

write_to_file(input_file_path, "Hello, Rust!").expect("Failed to write to

file");

// Читаем данные из файла

match read_file_content(input_file_path) {

Ok(content) => {

println!("Read content: {}", content);

// Записываем прочитанные данные в другой файл

write_to_file(output_file_path, &content).expect("Failed to write to

file");

println!("Data successfully written to {}", output_file_path);

}

Err(err) => {

eprintln!("Error reading file: {}", err);

}

}

}

Научиться контролировать многопоточность используя инструменты для безопасного параллельного выполнения, такие как потоки (threads) и примитивы синхронизации. Он также обеспечивает безопасность доступа к данным между потоками через систему собственности и заимствования.

use std::sync::{Arc, Mutex};

use std::thread;

// Общие данные, которые будут доступны из нескольких потоков

#[derive(Debug)]

struct SharedData {

counter: Mutex<u32>,

}

impl SharedData {
fn new() -> Self {

SharedData {

counter: Mutex::new(0),

}

}

// Метод для увеличения счетчика на 1

fn increment_counter(&self) {

// Заимствуем мьютекс для изменения данных

let mut counter = self.counter.lock().unwrap();

*counter += 1;

// Мьютекс автоматически освобождается при выходе

}
// Метод для получения текущего значения счетчика

fn get_counter(&self) -> u32 {

// Заимствуем мьютекс для чтения данных

let counter = self.counter.lock().unwrap();

*counter

// Мьютекс автоматически освобождается при выходе
}

}
fn main() {

// Arc (счетчик ссылок) для общих данных из разных потоков

let shared_data = Arc::new(SharedData::new());

// Клонируем Arc для использования в разных потоках

let thread_data1 = Arc::clone(&shared_data);

let thread_data2 = Arc::clone(&shared_data);

// Создаем потоки

let handle1 = thread::spawn(move || {

for _ in 0..5 {

thread_data1.increment_counter();

println!("Thread 1: Counter = {}", thread_data1.get_counter());

}

});

let handle2 = thread::spawn(move || {

for _ in 0..5 {

thread_data2.increment_counter();

println!("Thread 2: Counter = {}", thread_data2.get_counter());

}

});

// Дожидаемся завершения выполнения потоков

handle1.join().unwrap();

handle2.join().unwrap();

// основной поток может безопасно использовать данные

println!("Main thread: Final Counter = {}", shared_data.get_counter());

}


Научится работе с указателями.

#[derive(Debug)]

struct MyStruct {

value: i32,

}
fn main() {

// Создаем экземпляр MyStruct

let my_struct = MyStruct { value: 42 };

// Создаем неизменяемую ссылку (borrow) на my_struct

let reference = &my_struct;

// Обращаемся к данным через ссылку

println!("Value through reference: {}", reference.value);

// Создаем изменяемую ссылку (mutable borrow) на my_struct

let mut mutable_reference = my_struct;

// Изменяем данные через изменяемую ссылку

mutable_reference.value += 10;

// Печатаем измененное значение

println!("Updated value: {}", mutable_reference.value);

// владеющий (owning) указатель на MyStruct с использованием Box

let boxed_struct = Box::new(MyStruct { value: 99 });

// Распаковываем (dereference) и печатаем значение

println!("Value through Box: {}", (*boxed_struct).value);

// Rust автоматически управляет временем жизни ссылок и указателей,

// предотвращая типичные ошибки.
}