Data Exposure in Axum
How Data Exposure Manifests in Axum
Data exposure in Axum applications typically occurs through several specific patterns. The most common is improper handling of sensitive data in API responses, where developers inadvertently return more information than intended.
Consider an endpoint that retrieves user information:
async fn get_user(
id: u32,
db: Data<PgPool>,
) -> Json<User> {
let user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE id = $1",
id
)
.fetch_one(db.as_ref())
.await?;
Json(user) // Returns entire User struct including sensitive fields
}
The User struct might contain sensitive fields like password hashes, API keys, or internal IDs that should never be exposed to clients. Axum's type system doesn't automatically filter these fields during serialization.
Another common pattern involves error responses that leak implementation details. When database queries fail or validation errors occur, Axum handlers might return raw error messages:
async fn create_post(
Json(payload): Json<CreatePost>,
db: Data<PgPool>,
) -> Result<Json<Post>, (StatusCode, String)> {
let post = sqlx::query_as!(
Post,
"INSERT INTO posts (title, content) VALUES ($1, $2) RETURNING *",
payload.title,
payload.content
)
.fetch_one(db.as_ref())
.await?;
Ok(Json(post))
}
If the query fails, the raw database error might propagate to the client, revealing table names, schema details, or SQL syntax.
Path parameter handling can also lead to data exposure. Axum extracts path parameters as strings, but developers might use them to construct database queries without proper validation:
async fn get_order(
Path(order_id): Path<String>,
db: Data<PgPool>,
) -> Result<Json<Order>, StatusCode> {
let order = sqlx::query_as!(
Order,
"SELECT * FROM orders WHERE id = $1",
order_id
)
.fetch_one(db.as_ref())
.await?;
Ok(Json(order))
}
Without proper authorization checks, this allows any authenticated user to retrieve any order by guessing IDs, leading to horizontal privilege escalation.
Axum-Specific Detection
Detecting data exposure in Axum applications requires examining both the code structure and runtime behavior. Start by analyzing your route handlers for common patterns:
1. Response Struct Analysis
Examine all structs returned in Json responses. Look for fields that shouldn't be exposed to clients:
#[derive(Serialize)]
struct UserResponse {
id: i32,
email: String,
username: String,
// Missing: password_hash, api_key, internal_notes
}
async fn get_user_safe(
id: u32,
db: Data<PgPool>,
) -> Json<UserResponse> {
let user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE id = $1",
id
)
.fetch_one(db.as_ref())
.await?;
let response = UserResponse {
id: user.id,
email: user.email.clone(),
username: user.username.clone(),
};
Json(response)
}
2. Error Handling Inspection
Review all Result return types in your handlers. Ensure you're not returning raw error types:
async fn safe_create_post(
Json(payload): Json<CreatePost>,
db: Data<PgPool>,
) -> Result<Json<PostResponse>, ApiError> {
let post = sqlx::query_as!(
Post,
"INSERT INTO posts (title, content) VALUES ($1, $2) RETURNING *",
payload.title,
payload.content
)
.fetch_one(db.as_ref())
.await
.map_err(|e| ApiError::from(e))?;
Ok(Json(PostResponse::from(post)))
}
#[derive(Debug)]
enum ApiError {
Database(sqlx::Error),
Validation(String),
NotFound,
}
impl From<sqlx::Error> for ApiError {
fn from(err: sqlx::Error) -> Self {
match err {
sqlx::Error::RowNotFound => ApiError::NotFound,
_ => ApiError::Database(err),
}
}
}
impl From<ApiError> for (StatusCode, String) {
fn from(err: ApiError) -> Self {
match err {
ApiError::Database(_) =>
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".into()),
ApiError::Validation(msg) =>
(StatusCode::BAD_REQUEST, msg),
ApiError::NotFound =>
(StatusCode::NOT_FOUND, "Not found".into()),
}
}
}
3. Authorization Checks
Verify every data-accessing endpoint has proper authorization:
async fn get_user_with_auth(
Path(user_id): Path<i32>,
db: Data<PgPool>,
current_user: Option<AuthenticatedUser>,
) -> Result<Json<UserResponse>, StatusCode> {
let current_user = current_user.ok_or(StatusCode::UNAUTHORIZED)?;
// Only allow access to own profile or admin users
if current_user.id != user_id && !current_user.is_admin {
return Err(StatusCode::FORBIDDEN);
}
let user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE id = $1",
user_id
)
.fetch_one(db.as_ref())
.await?;
Ok(Json(UserResponse::from(user)))
}
4. Runtime Scanning with middleBrick
middleBrick's black-box scanning can identify data exposure vulnerabilities without requiring access to your source code. It tests endpoints by:
- Analyzing response payloads for sensitive data patterns (passwords, keys, tokens)
- Testing IDOR vulnerabilities by modifying path parameters
- Checking for excessive data in responses
- Verifying proper error handling doesn't leak implementation details
Run middleBrick on your API endpoints:
npx middlebrick scan https://api.example.com/users/123
The scanner will attempt to access data across different user contexts and analyze response structures for sensitive information exposure.
Axum-Specific Remediation
Remediating data exposure in Axum requires a layered approach using Axum's type system and middleware capabilities.
1. Response Type Isolation
Create dedicated response types that explicitly control what data is exposed:
#[derive(Serialize)]
struct UserProfile {
id: i32,
email: String,
created_at: DateTime<Utc>,
}
#[derive(Serialize)]
struct AdminProfile {
id: i32,
email: String,
created_at: DateTime<Utc>,
last_login: Option<DateTime<Utc>>,
account_status: String,
}
async fn get_profile(
Path(user_id): Path<i32>,
db: Data<PgPool>,
current_user: Option<AuthenticatedUser>,
) -> Result<Json<impl Serialize>, StatusCode> {
let current_user = current_user.ok_or(StatusCode::UNAUTHORIZED)?;
let user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE id = $1",
user_id
)
.fetch_one(db.as_ref())
.await?;
// Return different data based on authorization level
if current_user.id == user_id || current_user.is_admin {
if current_user.is_admin {
let admin_data = AdminProfile {
id: user.id,
email: user.email.clone(),
created_at: user.created_at,
last_login: user.last_login,
account_status: user.account_status,
};
return Ok(Json(admin_data));
}
let profile = UserProfile {
id: user.id,
email: user.email.clone(),
created_at: user.created_at,
};
return Ok(Json(profile));
}
Err(StatusCode::FORBIDDEN)
}
2. Error Handling Middleware
Create middleware to handle errors uniformly and prevent data leakage:
async fn error_handling_middleware(
req: Request,
next: Next,
) -> Result<Response, StatusCode> {
match next.run(req).await {
Ok(response) => Ok(response),
Err(err) => {
// Log detailed error internally
error!("Request failed: {:?}", err);
// Return generic error to client
let body = Json(json!({
"error": "An unexpected error occurred",
"request_id": uuid::Uuid::new_v4().to_string()
}));
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(body.into())?)
}
}
}
let app = Router::new()
.route("/api/users/:id", get(get_user_with_auth))
.layer(AddExtensionLayer::new(error_handling_middleware));
3. Authorization Middleware
Create reusable authorization middleware to prevent unauthorized data access:
async fn authorization_middleware(
req: Request,
next: Next,
db: Data<PgPool>,
) -> Result<Response, StatusCode> {
let user_id = req.extensions().get::();
let resource_id = req.extensions().get::();
if let (Some(user_id), Some(resource_id)) = (user_id, resource_id) {
// Check if user owns the resource or has admin privileges
let is_owner = sqlx::query!(
"SELECT 1 FROM resources
WHERE id = $1 AND owner_id = $2",
resource_id,
user_id
)
.fetch_optional(db.as_ref())
.await?;
if is_owner.is_none() {
let user = sqlx::query!(
"SELECT is_admin FROM users WHERE id = $1",
user_id
)
.fetch_one(db.as_ref())
.await?;
if !user.is_admin {
return Err(StatusCode::FORBIDDEN);
}
}
}
next.run(req).await
}
let app = Router::new()
.route("/api/resources/:id", get(get_resource))
.layer(AddExtensionLayer::new(authorization_middleware));
4. Data Sanitization
Implement sanitization for responses that must include user-provided data:
async fn get_public_profile(
Path(username): Path<String>,
db: Data<PgPool>,
) -> Result<Json<PublicProfile>, StatusCode> {
let user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE username = $1",
username
)
.fetch_one(db.as_ref())
.await?;
// Sanitize bio field to prevent XSS
let safe_bio = sanitize_html::sanitize_str(&user.bio);
let profile = PublicProfile {
username: user.username,
bio: safe_bio,
avatar_url: user.avatar_url.clone(),
created_at: user.created_at,
};
Ok(Json(profile))
}
5. Continuous Monitoring with middleBrick Pro
middleBrick Pro's continuous monitoring can automatically scan your APIs on a schedule:
# GitHub Action for continuous monitoring
name: API Security Scan
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run middleBrick Scan
run: |
npx middlebrick scan https://staging.example.com/api \
--output json \
--threshold B \
--fail-below B
- name: Upload Results
uses: actions/upload-artifact@v3
if: always()
with:
name: security-scan-results
path: middlebrick-report.json
This setup ensures your Axum APIs are continuously checked for data exposure vulnerabilities, with build failures triggered when security scores drop below your threshold.
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 |