Retour à tous les posts
Release

Le SAST Rust est du théâtre. On vient de livrer le vrai.

Premier ruleset Rust avec analyse de dataflow réelle. SQL injection sur sqlx, diesel, rusqlite et tokio-postgres. XSS, CORS, SSRF. Injecté dans chaque agent IA au moment du prompt, avec un taint tracking de bout en bout que les scanners par patterns ne savent pas faire.

Sur cette page
  1. Pourquoi le SAST Rust est resté du théâtre
  2. Ce qu'on vient de livrer, en un écran
  3. SQL injection, sur les quatre drivers Rust majeurs
  4. sqlx, requêtes async vérifiées à la compilation
  5. diesel, l'ORM typé
  6. rusqlite, SQLite embarqué
  7. tokio-postgres, le client PostgreSQL async
  8. Cross-Site Scripting (XSS) sur actix-web, axum et rocket
  9. Misconfigurations CORS sur les serveurs HTTP Rust
  10. Server-Side Request Forgery (SSRF) en Rust async
  11. Dataflow vs pattern matching, côte à côte
  12. Dans la loop de l'agent, pas juste dans le dashboard
  13. Ce qui arrive après pour Rust chez CybeDefend
  14. Récupérez le ruleset Rust

La plupart des outils d'analyse statique font semblant de couvrir Rust. Ils lancent un catalogue de regex, flaguent une liste de noms de fonctions dangereuses, et appellent ça de la couverture. Le vrai travail, suivre une entrée utilisateur à travers les async traits, les connexions de base de données génériques, les macros dérivées et les chaînes de middleware jusqu'à ce qu'elle atteigne une requête SQL ou une réponse HTML, n'a jamais été fait. Aujourd'hui, on livre le premier ruleset Rust avec analyse de dataflow réelle : taint tracking de bout en bout sur les quatre principaux drivers de base de données (sqlx, diesel, rusqlite, tokio-postgres), plus la détection XSS, CORS et SSRF. Tout ça vit dans notre Security Code Knowledge Graph et se propage dans chaque agent IA au moment du prompt.

Le Rust en production a grandi. Cloudflare fait tourner Pingora sur chaque requête. Discord a réécrit Read States. AWS livre Firecracker, Bottlerocket et une liste qui s'allonge de services en Rust. Stripe fait tourner du Rust sur les paths de paiement. Linkerd, Polkadot, Solana, Tauri, la liste s'allonge chaque mois. Pendant ce temps, le marché du static analysis a "rattrapé son retard" en ajoutant *.rs au glob de fichiers, en considérant le job terminé.

Cette faille n'est pas académique. C'est là que vivent les vulnérabilités.

Pourquoi le SAST Rust est resté du théâtre

L'analyse statique sur Rust est difficile pour des raisons qui n'apparaissent pas dans les pages marketing des vendors SAST classiques.

Les lifetimes. La durée de vie d'un &str dans une requête SQL détermine si une référence taintée est même encore vivante au moment où la requête tourne. Les scanners par patterns ignorent les annotations de lifetime parce qu'ils ne savent pas raisonner dessus ; ils traitent chaque &str comme s'il s'agissait d'un String. Résultat : soit un déluge de faux positifs, soit un miss silencieux.

L'async. Chaque service web Rust moderne est bâti sur tokio ou async-std. L'entrée utilisateur arrive dans une async fn et passe par des chaînes de .await, des adapters Pin<Box<dyn Future>>, des middlewares en trait-object. Un scanner par patterns voit le .await et arrête de suivre la valeur. Une vraie analyse de taint doit continuer.

Les génériques. sqlx::query(&str) est générique sur le type de connexion, le backend de la base de données et le type de ligne. Une requête typique passe par sqlx::QueryBuilder<'_, Postgres> et se termine par .execute(&mut conn)conn: &mut PoolConnection<Postgres>. Un scanner naïf ne sait pas que &mut PoolConnection<Postgres> et &mut PgConnection sont la même chose du point de vue du modèle de sécurité. Notre graphe normalise les deux vers un nœud "sql sink" et suit le taint à travers les deux.

Les macros dérivées. #[derive(sqlx::FromRow)], #[derive(diesel::Queryable)], les blanket impls actix_web::Responder : les macros se développent en code que les scanners par patterns ne voient jamais. Nous, on expand les macros d'abord, puis on walk le graphe résultant. Sinon le vrai sink est invisible.

Les trait objects. Un scanner qui pattern-match actix_web::HttpResponse::Ok().body(x) rate les mêmes données sortant par un impl Responder retourné par un handler. Forme syntaxique différente, propriété de sécurité identique.

Empile tout ça et Rust devient un langage où le pattern matching produit rapport sur rapport de faux positifs sur des lignes manifestement sûres, tout en ratant silencieusement les lignes qui ne le sont pas. Les équipes arrêtent de faire confiance au rapport. Le scanner se fait désactiver en CI. Vu en détail chez quatre clients cette année, rien que ça.

La solution n'est pas "plus de patterns". La solution, c'est le dataflow.

Ce qu'on vient de livrer, en un écran

Le ruleset Rust est live sur chaque région CybeDefend depuis aujourd'hui. Il couvre :

  • SQL injection sur les quatre drivers Rust majeurs, sqlx, diesel, rusqlite, tokio-postgres, avec un dataflow qui suit l'entrée utilisateur à travers les fonctions async, les types de connexion génériques et les helpers de requête générés par macro.
  • Cross-Site Scripting (XSS) sur les trois frameworks web Rust mainstream, actix-web, axum, rocket, y compris les réponses HTML produites via les moteurs de template askama et tera.
  • Misconfigurations CORS : wildcard Access-Control-Allow-Origin combiné avec des requêtes credentialed, preflight handling absent ou non sûr, echoes d'origine dangereusement permissifs.
  • Server-Side Request Forgery (SSRF) dans les clients HTTP async : reqwest, ureq, hyper et surf, avec propagation du taint à travers chaque URL builder helper.

Chaque finding embarque le data path complet à travers votre code : localisation source, chaque hop intermédiaire, localisation sink. Pas de "vulnérabilité détectée à la ligne 47" sans explication. De vrais findings avec leur trace, prêts pour Cybe AutoFix qui ouvre le patch.

SQL injection, sur les quatre drivers Rust majeurs

Les quatre drivers couvrent à peu près 95 % du trafic SQL Rust en production aujourd'hui (stats de download crates.io, 90 derniers jours). On les track tous les quatre avec des modèles de sink spécifiques à chaque librairie, pour que le dataflow ne perde pas le taint quand il traverse une frontière d'API.

sqlx, requêtes async vérifiées à la compilation

sqlx est le défaut moderne. Ses macros query! et query_as! vérifient le SQL contre la base à la compilation. Cette vérification ne vous protège pas contre l'interpolation.

use axum::{extract::Query, Json};
use serde::Deserialize;
use sqlx::PgPool;

#[derive(Deserialize)]
struct Params {
    name: String,
}

pub async fn search_users(
    pool: PgPool,
    Query(params): Query<Params>,
) -> Json<Vec<User>> {
    // La forme macro est vérifiée à la compilation, mais la forme
    // `query` prend un &str runtime. Concaténer l'entrée utilisateur
    // directement dans le SQL = interpolation, peu importe le driver.
    let sql = format!(
        "SELECT * FROM users WHERE name LIKE '%{}%'",
        params.name
    );
    let rows = sqlx::query_as::<_, User>(&sql)
        .fetch_all(&pool)
        .await
        .unwrap();
    Json(rows)
}

Source : params.name, qui vient de Query<Params> sur un axum::extract. Sink : sqlx::query_as(&sql) parce que le &str est construit par format!. Aucun sanitiser sur le chemin. Vraie SQL injection.

Le ruleset Rust Cybe trace le taint depuis le deserializer axum::extract::Query, à travers l'expansion de la macro format! (on expand format! avant de le walker), jusque dans sqlx::query_as, et le rapporte avec un chemin reachable à 100 %. Le fix :

let rows = sqlx::query_as::<_, User>(
    "SELECT * FROM users WHERE name LIKE $1"
)
.bind(format!("%{}%", params.name))
.fetch_all(&pool)
.await
.unwrap();

.bind() garde la valeur en dehors du texte SQL. Même forme syntaxique, propriété de sécurité complètement différente.

diesel, l'ORM typé

diesel est l'ORM typé heavyweight. La majeure partie de l'API est safe par construction : on construit ses requêtes avec le DSL et Diesel émet du SQL paramétrisé. La porte de sortie dangereuse, c'est diesel::sql_query.

use diesel::prelude::*;
use diesel::sql_query;

pub fn find_by_role(conn: &mut PgConnection, role: &str) -> Vec<User> {
    // Interpolation de variable directement dans du SQL brut, exécuté
    // par sql_query. Diesel n'alerte pas ; le type system laisse passer.
    let q = format!("SELECT * FROM users WHERE role = '{}'", role);
    sql_query(&q).load::<User>(conn).unwrap()
}

Cybe trace le taint depuis le paramètre &str role (qu'on modélise comme user-input-equivalent au boundary de fonction sauf si un sanitiser upstream est présent) jusque dans diesel::sql_query. Le fix utilise bind_param :

use diesel::sql_types::Text;

pub fn find_by_role(conn: &mut PgConnection, role: &str) -> Vec<User> {
    sql_query("SELECT * FROM users WHERE role = $1")
        .bind::<Text, _>(role)
        .load::<User>(conn)
        .unwrap()
}

rusqlite, SQLite embarqué

rusqlite est le binding SQLite que la plupart des outils desktop et services embarqués Rust utilisent. Sa forme dangereuse est la plus syntaxiquement évidente des quatre :

use rusqlite::Connection;

pub fn delete_by_name(conn: &Connection, name: String) {
    let sql = format!("DELETE FROM items WHERE name = '{}'", name);
    conn.execute(&sql, []).unwrap();
}

Cybe trace le taint jusque dans Connection::execute(&sql, []). Le fix :

conn.execute(
    "DELETE FROM items WHERE name = ?1",
    [&name],
).unwrap();

tokio-postgres, le client PostgreSQL async

tokio-postgres est le client async bas niveau qu'on prend quand on veut le contrôle total. Ses Client::query et Client::execute prennent un &str pour le statement, ouvrant le même trou.

use tokio_postgres::Client;

pub async fn revoke_session(client: &Client, session_id: String) {
    let sql = format!(
        "UPDATE sessions SET revoked = true WHERE id = '{}'",
        session_id
    );
    client.execute(&sql, &[]).await.unwrap();
}

Cybe le trace à travers le paramètre String jusque dans Client::execute. Fix :

client.execute(
    "UPDATE sessions SET revoked = true WHERE id = $1",
    &[&session_id],
).await.unwrap();

Cross-Site Scripting (XSS) sur actix-web, axum et rocket

Le XSS dans les serveurs web Rust vient d'une seule forme : un handler retourne du texte sous contrôle utilisateur dans du HTML sans l'échapper. Chaque framework Rust mainstream supporte ce footgun.

// actix-web
use actix_web::{get, web, HttpResponse, Responder};

#[get("/welcome")]
async fn welcome(name: web::Query<NameParams>) -> impl Responder {
    HttpResponse::Ok()
        .content_type("text/html")
        .body(format!("<h1>Welcome, {}</h1>", name.name))
}
// axum
use axum::{extract::Query, response::Html};

pub async fn welcome(Query(params): Query<NameParams>) -> Html<String> {
    Html(format!("<h1>Welcome, {}</h1>", params.name))
}
// rocket
use rocket::response::content::RawHtml;

#[get("/welcome?<name>")]
fn welcome(name: String) -> RawHtml<String> {
    RawHtml(format!("<h1>Welcome, {}</h1>", name))
}

Les trois sont du XSS stocké ou réfléchi selon la façon dont name arrive. Le ruleset Cybe normalise les trois frameworks vers un modèle de sink unique "html response" dans le graphe. Le dataflow suit le paramètre depuis l'extracteur (web::Query, axum::extract::Query, derive rocket::request::FromRequest) à travers le corps du handler jusqu'à la réponse. S'il n'y a pas de htmlescape::encode_minimal, askama::Html, ou équivalent sur le chemin, le finding est rapporté.

Un scanner qui fait uniquement du pattern voit HttpResponse::Ok().body(x) comme une forme de sink et la flague. Il ne sait pas si x était tainté ou si c'était un template de chaîne statique. Donc soit il flague chaque body de réponse et noie l'équipe sous le bruit, soit il skip la règle.

Misconfigurations CORS sur les serveurs HTTP Rust

Le bug CORS le plus courant en Rust, c'est wildcard d'origine combiné à des requêtes credentialed. Le navigateur refuse la combinaison, mais le développeur essaie de contourner en réfléchissant l'origine de la requête sans allowlist.

use tower_http::cors::{Any, CorsLayer};

let cors = CorsLayer::new()
    .allow_origin(Any)            // wildcard
    .allow_credentials(true)      // aussi true
    .allow_methods(Any);

C'est silencieusement cassé : les navigateurs refusent la combinaison, mais surtout, ça signale au security reviewer que le modèle mental du dev traite Origin comme un signal de trust. Notre ruleset le rapporte au moment de la construction, avec la raison précise. Autres façons de livrer le même bug, qu'on catch aussi :

  • Réfléchir request.headers().get("origin").unwrap() dans Access-Control-Allow-Origin sans allowlist.
  • .allow_origin(origin) depuis une valeur de config deserializée sans validation à la compilation.
  • Middleware CORS custom qui écrit le header directement, contournant tower-http.

Le dataflow suit la chaîne d'origine depuis la requête, à travers chaque middleware, jusque dans le header de réponse. Pattern matching sur Access-Control-Allow-Origin = "*" catch le cas un et rate les cas deux, trois et quatre.

Server-Side Request Forgery (SSRF) en Rust async

Le SSRF en Rust, ça ressemble à ça :

use reqwest::Client;
use axum::extract::Query;

pub async fn fetch_url(
    client: &Client,
    Query(params): Query<UrlParams>,
) -> Result<Vec<u8>, Error> {
    let resp = client.get(&params.url).send().await?;
    Ok(resp.bytes().await?.to_vec())
}

URL sous contrôle utilisateur, aucune allowlist, aucun check de scheme, aucun filtre IP. Le handler va joyeusement fetcher http://169.254.169.254/latest/meta-data/iam/security-credentials/ et retourner les credentials d'instance AWS au caller. Vu dans chaque codebase Rust qui touche à des URLs fournies par l'utilisateur.

Le ruleset Rust Cybe track le taint à travers :

  • reqwest::Client::get, post, put, delete, head, request
  • les appels chaînés sur reqwest::RequestBuilder
  • ureq::get, ureq::post, et l'agent builder
  • hyper::Client::request avec une Request<Body> construite manuellement
  • surf::get et surf::Client::send

Le sanitiser sur le chemin peut être une host allowlist, un URL parser qui rejette les plages CIDR internes, ou un wrapper qui résout le hostname et re-check l'IP résultante. On modélise les trois et on arrête le rapport quand un sanitiser est présent.

Dataflow vs pattern matching, côte à côte

Le plus simple pour sentir la différence entre dataflow et pattern matching, c'est de regarder du code que les scanners par patterns ratent et que le dataflow catch.

use sqlx::{PgPool, QueryBuilder, Postgres};

pub async fn search(
    pool: &PgPool,
    input: SearchInput,
) -> Result<Vec<Item>, sqlx::Error> {
    let mut q: QueryBuilder<Postgres> = QueryBuilder::new(
        "SELECT * FROM items WHERE 1 = 1"
    );

    if let Some(name) = input.name {
        // .push_bind() est safe (paramétré).
        // .push() est de la concaténation. Pure SQL injection.
        q.push(" AND name LIKE '%").push(&name).push("%'");
    }

    q.build_query_as::<Item>().fetch_all(pool).await
}

Un scanner par patterns qui cherche format! à côté de sqlx::query ne trouve rien. Pas de format!. Pas d'opérateur de concaténation de string. Le taint passe par QueryBuilder::push, une fonction qui est elle-même parfaitement safe quand appelée avec une string constante, et unsafe quand appelée avec une entrée utilisateur. Les scanners par patterns ne savent pas faire la différence parce qu'ils ne suivent pas la valeur.

Le ruleset Rust Cybe trace le taint depuis input.name (un Option<String> deserializé par serde et axum) à travers le .push(&name). QueryBuilder::push est enregistré dans notre graphe comme un sink SQL-text. Le finding est rapporté avec la trace complète. Les scanners par patterns livrent un rapport clean sur le même fichier.

C'est exactement le genre de bug pour lequel on a écrit le ruleset.

Dans la loop de l'agent, pas juste dans le dashboard

Les nouvelles règles Rust ne sont pas en train de dormir dans un rapport CI. Elles sont injectées dans chaque agent IA de codage via VibeDefend, la couche MCP-native qui vit à l'intérieur de Claude Code, Cursor, Windsurf, GitHub Copilot, OpenAI Codex, Cline, Continue et Zed.

Quand l'agent génère du code Rust, le ruleset est déjà chargé comme contexte. Si le diff introduit un chemin tainté vers un des sinks ci-dessus, l'agent réécrit le code avant que la ligne soit même suggérée au développeur. On n'arrête pas le diff à la review de PR. On l'arrête au prompt.

$ npx -y @cybedefend/vibedefend@latest install

 ✓ Détecté: Claude Code
 ✓ Chargé: 4,217 règles (ruleset Rust: 312)
 ✓ Memory: graphe projet synchronisé
 ✓ Prêt

Le prompt suivant du dev à Claude Code pour ajouter un endpoint de search renvoie la version paramétrée du premier coup. Pas de PR, pas de file d'attente de review, pas de ticket Jira.

Ce qui arrive après pour Rust chez CybeDefend

C'est le premier ruleset Rust, pas le dernier. Roadmap du prochain trimestre, dans l'ordre des commits :

  1. Path traversal et accès filesystem unsafe à travers std::fs, tokio::fs, async_std::fs, avec détection de sanitisers par allowlist.
  2. Deserialization unsafe pour serde_json::from_str sur des inputs untrusted, plus bincode, ciborium et la famille rmp_serde.
  3. Command injection à travers std::process::Command, tokio::process::Command, duct et la crate subprocess.
  4. WebAssembly host functions, modélisation des sinks pour les embeddings wasmtime et wasmer.
  5. Misuse cryptographique : RNG faibles dans les contextes de sécurité, hash functions faibles pour le stockage de password, IV hardcodées dans le chiffrement symétrique, flags ECB mode.
  6. Transport non sûr : TLS désactivé, custom certificate verifiers qui retournent Ok en toute circonstance.

Chaque règle dans cette liste livre avec son dataflow, pas avec un pattern. Chaque règle est poussée à l'agent dans le même cycle de release.

Récupérez le ruleset Rust

Le nouveau ruleset est live pour chaque client CybeDefend dès maintenant. Aucun flag à basculer, aucun upgrade. Si vous n'êtes pas encore sur la plateforme, l'install est en une commande :

npx -y @cybedefend/vibedefend@latest install

Cette commande auto-detecte votre agent IA de codage, charge le ruleset complet (Rust inclus) dans le contexte de l'agent, et commence à catcher les bugs au prochain prompt. Free tier, 50 crédits IA, sans CB.

Pour un tour plus poussé : prenez 30 min avec l'équipe. On pull votre repo en screen-share et on montre le dataflow sur de vraies lignes de votre code. Pas de slides, pas de SDR.

En live · tout juste sorti

Installez VibeDefend en 5 secondes.

Une commande. Chaque agent de coding sur votre laptop branché à CybeDefend: règles métier minées dans votre code, règles sécurité des frameworks que vos auditeurs attendent, action guards qui bloquent les appels dangereux avant qu'ils partent.

Installer en 5 secondesNode 18.17+
npx -y @cybedefend/vibedefend@latest install
Auto-détecte
  • Claude CodeClaude Code
  • CursorCursor
  • OpenAI Codex
  • WindsurfWindsurf
  • GitHub CopilotVS Code Copilot
Lire le README sur npm