Rust против SQL-инъекций и XSS: пишем веб-безопасно

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

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

Начать курс

Почему Rust — это новый стандарт безопасности в вебе?

Когда речь заходит о веб-безопасности, многие разработчики сразу вспоминают про ORM, подготовленные запросы и HTML-экранирование. Но что, если я скажу вам, что сам язык программирования может взять на себя львиную долю этой работы? Именно это и делает Rust — язык, который не прощает ошибок, но и не даёт их совершить.

В этой статье мы разберём, как безопасность Rust проявляется в двух самых критичных атаках на веб-приложения: SQL-инъекции и XSS (межсайтовый скриптинг). Вы увидите реальные примеры кода, сравнение с Python и поймёте, почему Rust становится выбором номер один для проектов, где цена ошибки — безопасность пользователей.

Как Rust предотвращает SQL-инъекции: от теории к практике

SQL-инъекция — это классика. Мы все знаем, что нужно использовать подготовленные запросы. Но в Rust этот паттерн встроен в культуру языка на уровне типов и системы borrow checker.

Сравнение подходов: Python vs Rust

Посмотрите на типичный код на Python, который может быть уязвим:

# Python (опасно)
user_input = "'; DROP TABLE users; --"
query = f"SELECT * FROM users WHERE name = '{user_input}'"
cursor.execute(query)

Даже если вы используете psycopg2, вы можете случайно забыть экранирование. Rust же заставляет вас мыслить иначе.

В Rust с библиотекой sqlx или diesel вы не сможете просто вставить строку в запрос — система типов не позволит:

// Rust с sqlx (безопасно)
use sqlx::PgPool;

let user_input = "'; DROP TABLE users; --";
let user: Option<User> = sqlx::query_as(
    "SELECT * FROM users WHERE name = $1"
)
.bind(&user_input) // Типобезопасная привязка
.fetch_optional(&pool)
.await?;

Обратите внимание на $1 и .bind(). Rust не позволяет вставить значение напрямую в строку запроса. Это не просто соглашение — это обязательное условие компиляции. Безопасность Rust здесь проявляется в том, что вы физически не можете создать SQL-инъекцию, если используете правильные библиотеки.

Почему это работает на уровне типов?

В Rust строка запроса и данные — это разные типы. Библиотеки вроде sqlx проверяют запросы на этапе компиляции (compile-time checked queries). Если вы попытаетесь сделать что-то опасное, код просто не скомпилируется:

// Это не скомпилируется!
let query = format!("SELECT * FROM users WHERE name = '{}'", user_input);
sqlx::query(&query); // Ошибка: ожидается &str, а не String с подстановкой

Система владения (ownership) и заимствования (borrowing) не даёт вам случайно "проглотить" опасные данные. Это не магия — это инженерная дисциплина, зашитая в язык.

Защита от XSS: экранирование и автоматическая проверка

XSS — это когда злоумышленник внедряет скрипты в ваш HTML. В Rust вы можете защититься на нескольких уровнях, причём некоторые проверки происходят ещё до запуска программы.

Шаблонизаторы с автоматическим экранированием

В Python вы используете jinja2 с autoescape=true. В Rust есть askama или maud, которые делают то же самое, но с типовой безопасностью. Вот пример с askama:

// Rust с askama (шаблонизатор)
use askama::Template;

#[derive(Template)]
#[template(source = "<h1>Hello, {{ name }}!</h1>", ext = "html")]
struct HelloTemplate {
    name: String,
}

fn main() {
    let t = HelloTemplate {
        name: "<script>alert('XSS')</script>".to_string(),
    };
    let rendered = t.render().unwrap();
    // Вывод: <h1>Hello, &lt;script&gt;...</h1>
    // Теги экранированы автоматически!
}

Обратите внимание: askama по умолчанию экранирует все переменные. Вам не нужно помнить про |safe или |e — это поведение встроено в язык шаблонов. Защита от XSS в Rust становится частью процесса разработки, а не дополнительной проверкой.

Ручная обработка: когда вам нужно больше контроля

Иногда вам нужно вывести HTML-теги специально (например, для форматированного текста). В таких случаях Rust предоставляет безопасные API:

// Ручное экранирование с помощью crate ammonia
use ammonia::clean;

let user_input = "<b>bold</b><script>alert('xss')</script>";
let safe_html = clean(&user_input); // Удаляет <script> и опасные атрибуты
println!("{}", safe_html); // <b>bold</b>

Библиотека ammonia — это как bleach для Python, но с Rust-подходом: она не только очищает, но и проверяет структуру HTML на корректность.

Сравнение с Python: где Rust выигрывает?

Давайте честно: Python с Django или FastAPI тоже может быть безопасным. Но разница — в подходе к ошибкам:

  • Python: "Мы надеемся, что разработчик не забудет экранировать". Большинство фреймворков делают это за вас, но вы всегда можете случайно обойти защиту.
  • Rust: "Вы не сможете это сделать, даже если захотите". Компилятор блокирует опасные паттерны на этапе сборки.

Это не значит, что Rust-приложения непробиваемы. Но безопасность Rust снижает вероятность человеческой ошибки на порядок. Для проектов, где уязвимость стоит миллионов (финансы, медицина, критическая инфраструктура), это критично.

Пример: обработка пользовательского ввода

В Python вы можете случайно использовать mark_safe() или забыть про |safe в шаблоне. В Rust такой код просто не скомпилируется, если тип не реализует нужный трейт:

// Rust: безопасный вывод с типом HtmlString
struct HtmlString(String);

impl HtmlString {
    fn from_unsafe(s: &str) -> Self {
        // Экранирование обязательно
        HtmlString(html_escape::encode_text(s).to_string())
    }
}

fn render(s: HtmlString) {
    // Можно использовать только безопасные строки
}

Видите? Вы не можете передать обычную строку в render() — нужно явно преобразовать её через from_unsafe(). Это заставляет разработчика думать о безопасности на каждом шагу.

Блоги

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