Добавить в корзинуПозвонить
Найти в Дзене
Один Rust не п...Rust

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

t.me/oneRustnoqRust систему типов с строгой проверкой во время компиляции, которая помогает избегать множества ошибок, таких как сегментация памяти, нулевые указатели и другие типичные ошибки безопасности. Представьте, что у вас есть книга с пронумерованными страницами. Если вы попробуете прочитать страницу 1000, а в книге всего 100 страниц — это и есть сегментация памяти. Представьте, что у вас есть адрес друга, по которому вы хотите отправить письмо. Но вместо адреса у вас написано "никуда". Если вы попытаетесь отправить письмо по такому адресу — получите ошибку. "владения" (ownership) и "заимствования" (borrowing), которые позволяют строго управлять доступом к данным и предотвращать гонки данных и неопределенное поведение. Владение — это право на данные. Передача владения означает, что старая переменная больше не может использовать данные. Заимствование — это временный доступ к данным без передачи владения. Можно читать или изменять данные, но по строгим правилам. действия с данным
Оглавление
ML на RUST без заморочек

t.me/oneRustnoqRust

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


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

систему типов с строгой проверкой во время компиляции, которая помогает избегать множества ошибок, таких как сегментация памяти, нулевые указатели и другие типичные ошибки безопасности. Представьте, что у вас есть книга с пронумерованными страницами. Если вы попробуете прочитать страницу 1000, а в книге всего 100 страниц — это и есть сегментация памяти. Представьте, что у вас есть адрес друга, по которому вы хотите отправить письмо. Но вместо адреса у вас написано "никуда". Если вы попытаетесь отправить письмо по такому адресу — получите ошибку.

"владения" (ownership) и "заимствования" (borrowing), которые позволяют строго управлять доступом к данным и предотвращать гонки данных и неопределенное поведение. Владение — это право на данные. Передача владения означает, что старая переменная больше не может использовать данные. Заимствование — это временный доступ к данным без передачи владения. Можно читать или изменять данные, но по строгим правилам.

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

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

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

строгие правила безопасности, такие как "Send" и "Sync" маркеры, что помогает избежать гонок данных и других проблем в параллельных программах. Представьте, что вы пишете программу, которая работает с банковскими счетами. В Rust компилятор не даст вам случайно удалить или испортить данные, если они ещё используются где-то в программе. У вас есть коробка с игрушками (Send). Вы можете безопасно передать её другу, и он сможет играть с игрушками, не сломав их. У вас есть общая доска для рисования (Sync). Несколько человек могут рисовать на ней одновременно, и никто не испортит рисунок другого. Представьте, что два человека одновременно пытаются пополнить один и тот же банковский счёт. Если они не синхронизированы, может получиться так, что деньги либо пропадут, либо запишутся дважды.

"unsafe" блоки, которые позволяют обойти строгие проверки безопасности, но при этом оставляют ответственность за безопасность на разработчике. unsafe — это способ сказать Rust: "Я знаю, что делаю, дай мне больше свободы". Внутри unsafe-блока можно делать опасные вещи, но только если вы уверены, что они безопасны. Ошибки в unsafe-коде могут привести к серьёзным проблемам, поэтому использовать его нужно осторожно и только когда это действительно необходимо.

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

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

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 автоматически управляет временем жизни ссылок и указателей,

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