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
-
Definizione della Proprietà
reverse_twice_prop: questa funzione verifica che invertire una lista due volte torni alla lista originale. -
Utilizzo di
proptest!Macro: la macroproptest!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. -
Verifica della Proprietà: per ogni input generato, la proprietà
reverse_twice_propviene verificata utilizzando l'asserzioneassert!, 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:
Resourcerappresenta una struttura dati per le risorse, con unidunivoco, unnameeowner_idche 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'headerAuthorization. Inoltre la funzioneget_user_id_from_tokendecodifica 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
GETall'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 diresource_ideuser_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
USERSper 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.