Test Property-Based con proptest-rs

I test property-based sono una metodologia di testing che si basa sulla generazione automatica di input per verificare le proprietà del software in condizioni diverse e estreme. In Rust, proptest-rs è una libreria potente e flessibile che facilita la creazione di test property-based, garantendo una copertura più completa e una maggiore affidabilità del codice.

Introduzione a proptest-rs

proptest-rs permette di definire proprietà del software e genera automaticamente input che le violano, consentendo di scoprire edge case e comportamenti inaspettati. Questo approccio si distingue dai test tradizionali, dove gli input sono staticamente definiti, e permette di esplorare in modo più completo lo spazio delle possibilità del software. Per iniziare, aggiungiamo proptest come dipendenza nel nostro file Cargo.toml:

[dev-dependencies]
proptest = "1.0"

Successivamente, definiamo un test property-based nel file di test Rust:

#![allow(unused)]
fn main() {
use proptest::prelude::*;

#[test]
fn test_reverse() {
    // Definiamo la proprietà: invertire una lista due volte torna alla lista originale
    fn reverse_twice_prop(input: Vec<i32>) -> bool {
        let reversed = input.iter().rev().cloned().collect::<Vec<_>>();
        let twice_reversed = reversed.iter().rev().cloned().collect::<Vec<_>>();
        input == twice_reversed
    }

    // Configuriamo proptest per generare input
    proptest!(|(input in prop::collection::vec(-100..100, 1..1000))| {
        // Verifichiamo la proprietà per ogni input generato
        assert!(reverse_twice_prop(input));
    });
}
}

In questo esempio si può trovare

  1. Definizione della Proprietà reverse_twice_prop: questa funzione verifica che invertire una lista due volte torni alla lista originale.

  2. Utilizzo di proptest! Macro: la macro proptest! genera input casuali basati sulle specifiche fornite. Nel nostro caso, genera liste di numeri interi nel range da -100 a 100, con dimensioni variabili da 1 a 999 elementi.

  3. Verifica della Proprietà: per ogni input generato, la proprietà reverse_twice_prop viene verificata utilizzando l'asserzione assert!, assicurando che la proprietà sia soddisfatta per ogni caso generato.

Esempio di Prevenzione di IDOR in Rust con actix-web

Per prevenire IDOR (Insecure Direct Object Reference) in una API web scritta in Rust con actix-web, possiamo implementare controlli di autorizzazione adeguati e utilizzare test property-based con proptest-rs per verificare che l'accesso agli oggetti sia correttamente limitato agli utenti autorizzati.

Supponiamo di avere un sistema di gestione di risorse dove gli utenti possono accedere solo alle risorse a cui sono autorizzati. Utilizzeremo JSON Web Token (JWT) per l'autenticazione e l'autorizzazione degli utenti.

use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use jsonwebtoken::{decode, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

// Struttura per rappresentare una risorsa
#[derive(Debug, Serialize, Deserialize)]
struct Resource {
    id: u64,
    name: String,
    owner_id: u64,
}

// Dati per simulare un database di risorse
lazy_static::lazy_static! {
    static ref RESOURCES: HashMap<u64, Resource> = {
        let mut map = HashMap::new();
        map.insert(1, Resource { id: 1, name: "Resource 1".to_string(), owner_id: 1 });
        map.insert(2, Resource { id: 2, name: "Resource 2".to_string(), owner_id: 2 });
        map.insert(3, Resource { id: 3, name: "Resource 3".to_string(), owner_id: 1 });
        map
    };
}

// Dati per simulare utenti autorizzati (id e JWT)
lazy_static::lazy_static! {
    static ref USERS: HashMap<u64, String> = {
        let mut map = HashMap::new();
        map.insert(1, generate_jwt_token(1));
        map.insert(2, generate_jwt_token(2));
        map
    };
}

// Funzione per generare un JWT token per un utente con un dato id
fn generate_jwt_token(user_id: u64) -> String {
    let claims = serde_json::json!({ "sub": user_id });
    jsonwebtoken::encode(&jsonwebtoken::Header::default(), &claims, &DecodingKey::from_secret("secret".as_ref())).unwrap()
}

// Funzione per verificare e ottenere l'id dell'utente dal token JWT
fn get_user_id_from_token(token: &str) -> Option<u64> {
    let decoding_key = DecodingKey::from_secret("secret".as_ref());
    let validation = Validation::default();

    match decode::<HashMap<String, serde_json::Value>>(token, &decoding_key, &validation) {
        Ok(token_data) => {
            if let Some(user_id) = token_data.claims.get("sub") {
                if let Some(user_id) = user_id.as_u64() {
                    return Some(user_id);
                }
            }
            None
        },
        Err(_) => None,
    }
}

// Handler per ottenere una risorsa
async fn get_resource(info: web::Path<u64>, auth_header: web::HeaderMap) -> impl Responder {
    // Estraiamo il JWT token dall'header Authorization
    let token = auth_header.get("Authorization")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.strip_prefix("Bearer "))
        .unwrap_or("");

    // Verifichiamo il token JWT e otteniamo l'id dell'utente
    let user_id = match get_user_id_from_token(token) {
        Some(user_id) => user_id,
        None => return HttpResponse::Unauthorized().body("Unauthorized"),
    };

    // Cerchiamo la risorsa nel database
    if let Some(resource) = RESOURCES.get(&info.into_inner()) {
        // Verifichiamo che l'utente sia autorizzato ad accedere alla risorsa
        if resource.owner_id == user_id {
            HttpResponse::Ok().json(resource)
        } else {
            HttpResponse::Forbidden().body("Accesso negato")
        }
    } else {
        HttpResponse::NotFound().body("Risorsa non trovata")
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/resources/{id}", web::get().to(get_resource))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

In questo esempio si trovano le seguenti parti:

  • Implementazione dell'API:

    • Resource rappresenta una struttura dati per le risorse, con un id univoco, un name e owner_id che identifica il proprietario della risorsa.
    • RESOURCES è una mappa simulata che contiene le risorse nel sistema.
    • USERS è una mappa che associa gli id degli utenti ai loro token JWT simulati.
  • Verifica dell'Accesso: nel handler get_resource, estraiamo e verifichiamo il token JWT dall'header Authorization. Inoltre la funzione get_user_id_from_token decodifica e verifica il token JWT, estraendo l'id dell'utente.

  • Controllo di Autorizzazione: dopo aver ottenuto l'id dell'utente dal token JWT, verifichiamo se l'utente è il proprietario della risorsa richiesta. Se non è il proprietario, restituiamo una risposta Forbidden.

  • Gestione delle Risposte: il server gestisce le richieste GET all'endpoint /resources/{id}. Restituisce la risorsa se trovata, altrimenti restituisce un messaggio di errore appropriato.

Test Property-Based per Prevenire IDOR

Possiamo utilizzare proptest-rs per verificare che solo gli utenti autorizzati possano accedere alle loro risorse. Definiamo un test che generi input casuali di id risorsa e token JWT, verificando che solo gli utenti autorizzati possano accedere alle risorse corrette:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::test;

    #[test]
    fn test_get_resource_authorization() {
        proptest!(|(resource_id in 1..=3u64, user_id in 1..=2u64)| {
            // Simuliamo il token JWT per l'utente corrente
            let token = USERS.get(&user_id).unwrap().clone();

            // Costruiamo una richiesta HTTP simulata con il token JWT
            let mut app = test::init_service(
                App::new().route("/resources/{id}", web::get().to(get_resource))
            ).unwrap();
            let req = test::TestRequest::get()
                .uri(&format!("/resources/{}", resource_id))
                .header("Authorization", format!("Bearer {}", token))
                .to_request();

            // Eseguiamo la richiesta
            let resp = test::call_service(&mut app, req);
            
            // Verifichiamo l'autorizzazione
            if let Some(resource) = RESOURCES.get(&resource_id) {
                if resource.owner_id == user_id {
                    assert_eq!(resp.status(), http::StatusCode::OK);
                } else {
                    assert_eq!(resp.status(), http::StatusCode::FORBIDDEN);
                }
            } else {
                assert_eq!(resp.status(), http::StatusCode::NOT_FOUND);
            }
        });
    }
}
}

In questo test property-based andiamo a definire le seguenti parti:

  • Definizione del Test: utilizziamo proptest! per generare input casuali di resource_id e user_id, verificando che la risposta della richiesta HTTP sia coerente con le regole di autorizzazione definite nell'API.
  • Simulazione del Token JWT: utilizziamo i token JWT simulati da USERS per simulare l'autenticazione degli utenti nelle richieste di test.
  • Verifica dell'Autorizzazione: verifichiamo che solo gli utenti autorizzati possano accedere alle risorse corrispondenti, con una corretta gestione delle risposte HTTP in base alle regole di autorizzazione.