Email Injection in Axum with Firestore
Email Injection in Axum with Firestore — how this specific combination creates or exposes the vulnerability
Email injection in an Axum application that uses Firestore typically occurs when user-controlled input is concatenated into email headers or message bodies without validation or encoding. Axum is a Rust web framework that relies on structured handlers, but developers may pass unchecked request parameters into email construction logic. Firestore, as a backend datastore, may store user profiles, contact entries, or communication logs that include email addresses used later by an Axum service to send messages.
When an Axum endpoint accepts a email or reply_to parameter and directly interpolates it into SMTP or an email template, an attacker can inject additional headers such as Cc:, Bcc:, or newline characters to trigger unauthorized messaging. Firestore documents that contain user-supplied email fields may be read by the Axum service and passed to an email library; if the service does not sanitize or validate these values, the injection surface is effectively extended to the backend datastore. This becomes critical when the Axum application uses service account credentials to read from Firestore and then uses that data in email workflows without strict schema enforcement.
The risk is compounded when Firestore documents include dynamic fields that influence email routing or templates. For example, a stored user profile may include a preferred_email field used by Axum to address notifications. If that field contains newline characters or header-like syntax, and Axum does not validate or encode it, the application may inadvertently send emails to unintended recipients or inject malicious headers. Because Firestore does not enforce email format constraints, Axum must treat all retrieved email values as untrusted input.
An attacker may not need direct access to Firestore to exploit this issue; they can provide malicious input through HTTP request parameters that are stored in Firestore and later used by Axum. This chained path — user input stored in Firestore, retrieved by Axum, and used in email construction — creates a persistent injection vector. Standard email injection payloads such as newline sequences (\r\n) or header keywords (Cc:, Subject:) can be leveraged to alter the message routing or inject additional recipients.
Because middleBrick tests unauthenticated attack surfaces, it can detect injection points where Axum endpoints accept email-related inputs without proper validation. The scanner checks for input validation weaknesses and data exposure risks that may allow injection through stored Firestore data. Remediation requires both input sanitization at the Axum handler level and schema constraints in Firestore to reduce the likelihood of malicious email content being persisted and later used.
Firestore-Specific Remediation in Axum — concrete code fixes
To secure the Axum and Firestore combination, validate and sanitize all email inputs before storing them in Firestore and before using stored values in email construction. Use strict allow-lists for email formats and reject any content containing newline characters or header-like tokens. In Axum, apply validation layers in your extractor or handler so that only safe values reach Firestore and downstream email logic.
Below is a concrete Axum handler example that validates email input before writing to Firestore using the firestore_rs crate. The handler uses a regex pattern to enforce RFC-compliant email syntax and rejects inputs containing carriage returns or line feeds.
use axum::{
extract::Query, http::StatusCode, response::IntoResponse,
routing::get, Router,
};
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
struct EmailQuery {
email: String,
}
#[derive(Debug, Serialize)]
struct ApiResponse {
message: String,
}
fn is_valid_email(email: &str) -> bool {
// Basic RFC 5322-ish validation, reject newlines and control chars
let email_re = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
email_re.is_match(email) && !email.contains(|c| c == '\r' || c == '\n')
}
async fn send_notification(Query(params): Query) -> impl IntoResponse {
if !is_valid_email(¶ms.email) {
return (StatusCode::BAD_REQUEST, ApiResponse { message: "Invalid email".into() }).into_response();
}
// Store validated email in Firestore
let db = firestore_rs::Client::new("my-project-id");
let user_doc = firestore_rs::Document {
fields: vec![
firestore_rs::Field {
name: "email".to_string(),
value: firestore_rs::Value::String(params.email.clone()),
},
],
..Default::default()
};
db.create("users", Some(¶ms.email), user_doc).await;
(StatusCode::OK, ApiResponse { message: "Stored".into() }).into_response()
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/notify", get(send_notification));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
When reading from Firestore and constructing emails, re-validate the stored email values instead of assuming they are safe. The following example shows how to sanitize a Firestore document field before using it with an email library, preventing header injection even if legacy documents contain malicious content.
use axum::{
extract::Path, http::StatusCode, response::IntoResponse,
routing::get, Router,
};
async fn get_user_email(Path(user_id): Path) -> impl IntoResponse {
let db = firestore_rs::Client::new("my-project-id");
match db.get::(&"users", &user_id).await {
Ok(doc) => {
let raw_email = doc.get_field_string("email");
if let Some(safe_email) = sanitize_email(&raw_email) {
// safe_email can now be used in email headers
(StatusCode::OK, format!("Email: {}", safe_email)).into_response()
} else {
(StatusCode::BAD_REQUEST, "Invalid stored email".into_response())
}
}
Err(_) => (StatusCode::NOT_FOUND, "User not found".into_response()),
}
}
fn sanitize_email(value: &str) -> Option {
if value.contains(|c| c == '\r' || c == '\n' || c == ':') {
return None;
}
Some(value.trim().to_string())
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct UserDocument {
email: String,
}
impl UserDocument {
fn get_field_string(&self, field: &str) -> String {
match field {
"email" => self.email.clone(),
_ => String::new(),
}
}
}
Complement these code-level measures with Firestore schema design that avoids storing free-form email fields when possible. Use structured fields with strict validation rules enforced at write time. Combine Axum request validation and Firestore data constraints to ensure that email values remain safe throughout their lifecycle, reducing both injection risk and data exposure.