Insufficient Logging in Axum with Firestore
Insufficient Logging in Axum with Firestore — how this specific combination creates or exposes the vulnerability
Insufficient logging in an Axum application that uses Google Cloud Firestore as a backend reduces visibility into authentication, authorization, and data access events. Without structured logs that capture request identifiers, user identities, Firestore document paths, and operation outcomes, security monitoring and incident response are impaired. This gap is especially relevant for the BOLA/IDOR and Property Authorization checks performed by middleBrick, because missing logs prevent detection of unauthorized document reads or updates.
When Axum handlers interact with Firestore via the official Firestore SDK, each database call should be accompanied by a log entry that records the operation type, document reference, caller context (if any), and result status. If these entries are omitted or left at default log levels, an attacker may exploit weak access controls without leaving an actionable trace. middleBrick’s checks for BOLA/IDOR and Property Authorization rely on runtime behavior; insufficient logging means these checks may lack the audit trail needed to confirm whether unauthorized access occurred.
In practice, an Axum service might accept a document ID from a request, build a Firestore path such as users/{user_id}/records/{document_id}, and perform a read or write without validating that the authenticated subject is permitted to access that specific document. Without logs that include the subject identifier and the exact document path, there is no reliable way to reconstruct access patterns after an incident. This makes it harder to correlate findings from middleBrick’s runtime tests with actual access behavior, and it weakens compliance mappings to frameworks such as OWASP API Top 10 and SOC2.
Additionally, Firestore operations can fail silently in application code if errors are not captured and logged with sufficient context. For example, a missing permission might result in an empty result set rather than an explicit error, and if the Axum handler does not log this outcome, the team may not realize that authorization logic is being bypassed or that data exposure is occurring. middleBrick’s Data Exposure and Authentication checks depend on observable behaviors; without logs, it is difficult to verify whether the exposed behavior is a false positive or a genuine gap.
Structured logging with correlation IDs can mitigate these risks by ensuring each request can be traced across Axum middleware and Firestore calls. Including the user identity (when available), the Firestore document path, the operation type, and the response status in structured logs enables automated analysis and faster triage. This approach aligns with best practices for auditability and supports the remediation guidance provided by tools like middleBrick, which highlights the need for explicit authorization checks and observability in API implementations.
Firestore-Specific Remediation in Axum — concrete code fixes
To address insufficient logging in Axum when using Firestore, instrument your handlers to capture structured logs for each Firestore interaction. Include the request identifier, user context, document path, operation, and outcome. The following example shows an Axum handler that logs before and after a Firestore get, update, and delete, using the tracing crate for structured diagnostics.
use axum::{routing::get, Router, extract::State};
use google_cloud_firestore::client::Client;
use serde_json::json;
use std::net::SocketAddr;
use tracing::{info, error};
struct AppState {
firestore_client: Client,
}
async fn get_record_handler(
State(state): State<AppState>,
axum::extract::Path((user_id, doc_id)): axum::extract::Path<(String, String)>,
) -> Result<axum::Json<serde_json::Value>, (axum::http::StatusCode, String)> {
let document_path = format!("users/{}/records/{}", user_id, doc_id);
info!(
request_id = %axum::extract::request::RequestId::from_request(&axum::extract::Request::default()),
user_id = %user_id,
document_path = %document_path,
operation = "get",
"Firestore read attempted"
);
let doc_ref = state.firestore_client.doc(&document_path);
match doc_ref.get().await {
Ok(doc) => {
if doc.exists() {
info!(
request_id = %axum::extract::request::RequestId::from_request(&axum::extract::Request::default()),
user_id = %user_id,
document_path = %document_path,
operation = "get",
result = "success",
"Firestore read succeeded"
);
Ok(axum::Json(doc.data().unwrap_or_default()))
} else {
info!(
request_id = %axum::extract::request::RequestId::from_request(&axum::extract::Request::default()),
user_id = %user_id,
document_path = %document_path,
operation = "get",
result = "not_found",
"Firestore document does not exist"
);
Err((axum::http::StatusCode::NOT_FOUND, "Not found".to_string()))
}
}
Err(e) => {
error!(
request_id = %axum::extract::request::RequestId::from_request(&axum::extract::Request::default()),
user_id = %user_id,
document_path = %document_path,
operation = "get",
error = %e.to_string(),
"Firestore read failed"
);
Err((axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}
}
}
}
async fn update_record_handler(
State(state): State<AppState>,
axum::extract::Path((user_id, doc_id)): axum::extract::Path<(String, String)>,
axum::Json(payload): axum::Json<serde_json::Value>,
) -> Result<axum::Json<serde_json::Value>, (axum::http::StatusCode, String)> {
let document_path = format!("users/{}/records/{}", user_id, doc_id);
info!(
request_id = %axum::extract::request::RequestId::from_request(&axum::extract::Request::default()),
user_id = %user_id,
document_path = %document_path,
operation = "update",
payload = ?payload,
"Firestore update attempted"
);
let doc_ref = state.firestore_client.doc(&document_path);
match doc_ref.set(payload.clone()).await {
Ok(_) => {
info!(
request_id = %axum::extract::request::RequestId::from_request(&axum::extract::Request::default()),
user_id = %user_id,
document_path = %document_path,
operation = "update",
result = "success",
"Firestore update succeeded"
);
Ok(axum::Json(payload))
}
Err(e) => {
error!(
request_id = %axum::extract::request::RequestId::from_request(&axum::extract::Request::default()),
user_id = %user_id,
document_path = %document_path,
operation = "update",
error = %e.to_string(),
"Firestore update failed"
);
Err((axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}
}
}
async fn delete_record_handler(
State(state): State<AppState>,
axum::extract::Path((user_id, doc_id)): axum::extract::Path<(String, String)>,
) -> Result<axum::http::StatusCode, (axum::http::StatusCode, String)> {
let document_path = format!("users/{}/records/{}", user_id, doc_id);
info!(
request_id = %axum::extract::request::RequestId::from_request(&axum::extract::Request::default()),
user_id = %user_id,
document_path = %document_path,
operation = "delete",
"Firestore delete attempted"
);
let doc_ref = state.firestore_client.doc(&document_path);
match doc_ref.delete().await {
Ok(_) => {
info!(
request_id = %axum::extract::request::RequestId::from_request(&axum::extract::Request::default()),
user_id = %user_id,
document_path = %document_path,
operation = "delete",
result = "success",
"Firestore delete succeeded"
);
Ok(axum::http::StatusCode::NO_CONTENT)
}
Err(e) => {
error!(
request_id = %axum::extract::request::RequestId::from_request(&axum::extract::Request::default()),
user_id = %user_id,
document_path = %document_path,
operation = "delete",
error = %e.to_string(),
"Firestore delete failed"
);
Err((axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}
}
}
pub fn build_app() -> Router {
// Assume `AppState` with initialized Firestore client is provided
Router::new()
.route("/records/:user_id/:doc_id", get(get_record_handler))
.route("/records/:user_id/:doc_id", axum::routing::patch(update_record_handler))
.route("/records/:user_id/:doc_id", axum::routing::delete(delete_record_handler))
}