Data Exposure in Actix
How Data Exposure Manifests in Actix
Data exposure in Actix applications typically occurs when sensitive information is inadvertently returned in API responses or error messages. This vulnerability can manifest through several Actix-specific patterns:
- Debug mode leakage: When Actix runs in debug mode, it may include stack traces and internal server details in error responses, exposing file paths, environment variables, and application structure.
- Default error handlers: Actix's default error handlers can reveal internal state through detailed error messages that include database query information or request context.
- Logging configuration issues: Actix applications often log request bodies or headers that may contain sensitive data like passwords, tokens, or personal information.
- Serialization of internal structs: Accidentally serializing internal data structures that contain sensitive fields when only a subset should be exposed.
Here's a common Actix pattern that leads to data exposure:
async fn get_user_profile(info: web::Path<(String, String)>) -> impl Responder {
let (username, token) = info.into_inner();
// Database query that might fail
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE username = $1")
.bind(username)
.fetch_one('db)
.await;
match user {
Ok(user) => {
// Exposing internal database structure directly
HttpResponse::Ok().json(user)
}
Err(_) => {
// Debug mode may expose internal error details
HttpResponse::InternalServerError().body("Database error occurred")
}
}
}
In this example, the User struct likely contains fields like password_hash, internal_ids, or other sensitive information that should never be exposed to API consumers. The error handler also doesn't sanitize potential database errors.
Actix-Specific Detection
Detecting data exposure in Actix applications requires examining both the code structure and runtime behavior. Here are Actix-specific detection methods:
Code Analysis
Look for these patterns in your Actix codebase:
// Dangerous: Exposing internal struct directly
#[derive(Serialize)]
struct User {
id: i32,
username: String,
email: String,
password_hash: String, // This should never be exposed!
ssn: String, // Sensitive data exposure
}
// Safer: Using a dedicated response DTO
#[derive(Serialize)]
struct UserProfile {
id: i32,
username: String,
email: String,
}
Scan your Actix handlers for:
- Direct serialization of database models or internal structs
- Missing #[serde(skip)] attributes on sensitive fields
- Debug mode enabled in production (check RUST_LOG and environment variables)
- Default error handlers without custom sanitization
Runtime Testing
middleBrick's black-box scanning can detect data exposure by:
- Analyzing API responses for sensitive data patterns (passwords, tokens, SSNs, credit card numbers)
- Checking for stack traces and internal error messages in responses
- Testing error conditions to see what information is revealed
- Scanning OpenAPI specs for fields marked as sensitive but still included in responses
You can also test manually by:
# Test with invalid input to trigger error responses
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"wrong"}'
# Check if debug information is exposed
curl -X GET http://localhost:8080/nonexistent-endpoint
Look for responses containing:
- Stack traces or file paths
- Database error details
- Internal IDs or system information
- Default Actix error messages that reveal implementation details
Actix-Specific Remediation
Remediating data exposure in Actix requires a combination of code patterns and configuration changes. Here are Actix-specific solutions:
1. Use Dedicated Response DTOs
Never serialize internal structs directly. Create specific response types:
// Internal model (kept private)
#[derive(Debug, sqlx::FromRow)]
struct User {
id: i32,
username: String,
email: String,
password_hash: String,
ssn: String,
internal_id: Uuid,
}
// Public response model
#[derive(Serialize)]
struct UserResponse {
id: i32,
username: String,
email: String,
}
// Handler with proper mapping
async fn get_user_profile(
path: web::Path<i32>,
pool: web::Data<PgPool>
) -> Result<impl Responder> {
let user_id = path.into_inner();
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(user_id)
.fetch_one('pool)
.await?;
let response = UserResponse {
id: user.id,
username: user.username,
email: user.email,
};
Ok(HttpResponse::Ok().json(response))
}
2. Custom Error Handling
Replace Actix's default error handlers with sanitized versions:
use actix_web::{error, dev, http::StatusCode, HttpResponse};
use serde::Serialize;
#[derive(Serialize)]
struct ErrorResponse {
error: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
request_id: Option<String>,
}
pub fn create_error_handler() -> error::ErrorHandler<actix_web::Error> {
error::ErrorHandler::new(|err, req| {
let request_id = uuid::Uuid::new_v4().to_string();
// Log the actual error internally
log::error!("Request {} failed: {:?}", request_id, err);
// Return sanitized response
let response = ErrorResponse {
error: "internal_error".to_string(),
message: "An internal error occurred. Please try again later.".to_string(),
request_id: Some(request_id),
};
Ok(HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR)
.content_type("application/json")
.body(serde_json::to_string(&response).unwrap()))
})
}
// Register in main
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.wrap(create_error_handler())
.service(user_profile)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
3. Field-Level Serialization Control
Use serde attributes to control field exposure:
#[derive(Serialize)]
struct User {
id: i32,
username: String,
email: String,
#[serde(skip)]
password_hash: String,
#[serde(skip)]
ssn: String,
#[serde(skip)]
internal_id: Uuid,
#[serde(rename = "createdAt")]
created_at: DateTime<Utc>,
}
4. Configure Debug Mode
Ensure debug mode is disabled in production:
// In main.rs or lib.rs
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "info");
std::env::set_var("RUST_BACKTRACE", "0");
HttpServer::new(|| {
App::new()
.wrap(middleware::Logger::default())
.service(user_profile)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
5. Input Validation and Sanitization
Validate and sanitize all inputs before processing:
use actix_web::{web, FromRequest, HttpResponse};
use serde::Deserialize;
use validator::Validate;
#[derive(Deserialize, Validate)]
struct LoginRequest {
#[validate(email)]
email: String,
#[validate(length(min = 8))]
password: String,
}
async fn login(
login_data: web::Json<LoginRequest>,
pool: web::Data<PgPool>
) -> Result<impl Responder> {
// Validate input
login_data.validate()?;
// Process without logging sensitive data
let user = authenticate_user(&login_data.email, &login_data.password, 'pool).await?;
// Return only necessary data
Ok(HttpResponse::Ok().json(&user.public_profile()))
}
Related CWEs: dataExposure
| CWE ID | Name | Severity |
|---|---|---|
| CWE-200 | Exposure of Sensitive Information | HIGH |
| CWE-209 | Error Information Disclosure | MEDIUM |
| CWE-213 | Exposure of Sensitive Information Due to Incompatible Policies | HIGH |
| CWE-215 | Insertion of Sensitive Information Into Debugging Code | MEDIUM |
| CWE-312 | Cleartext Storage of Sensitive Information | HIGH |
| CWE-359 | Exposure of Private Personal Information (PII) | HIGH |
| CWE-522 | Insufficiently Protected Credentials | CRITICAL |
| CWE-532 | Insertion of Sensitive Information into Log File | MEDIUM |
| CWE-538 | Insertion of Sensitive Information into Externally-Accessible File | HIGH |
| CWE-540 | Inclusion of Sensitive Information in Source Code | HIGH |