Axum Request Validation and Data Sanitization in Rust Explained
When building web applications in Rust using the Axum framework, ensuring that the data your server receives is both valid and safe is a top priority. Request validation and data sanitization are essential steps in protecting your app from invalid inputs and malicious attacks like SQL injection or cross-site scripting (XSS).
In this tutorial, we’ll explore how to handle both request validation and data sanitization in Axum. By the end, you'll understand how to properly validate user inputs, ensure data integrity, and sanitize unsafe data.
Let’s build it step-by-step!
Step 1: Setting up the Axum Project
First, let's create a new Rust project and add Axum as a dependency. If you haven’t already, you can install Cargo (Rust's package manager) and create a new project:
cargo new axum-validation
cd axum-validation
Now, in the Cargo.toml
file, add Axum and Tokio as dependencies:
[dependencies]
axum = "0.6"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
validator = { version = "0.14", features = ["derive"] }
ammonia = "4.0.0"
We'll use the validator
crate for request validation and ammonia
crate for data sanitization.
Step 2: A Minimal "Hello, World!" Axum Server
Let’s start by building a simple "Hello, World!" server to ensure everything is set up correctly:
In your main.rs
file:
use axum::{
routing::get,
Router,
};
use tokio::main;
#[main]
async fn main() {
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
// Run the server
axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
This simple server listens on 127.0.0.1:3000 and responds with "Hello, World!" when you access the root URL. Run the server with cargo run
and visit http://localhost:3000/
to see it in action.
Step 3: Adding Request Validation
Now, let's introduce the concept of request validation. We'll be creating a POST endpoint that expects a JSON body, and we'll validate the data before processing it.
We’ll define a struct with fields that we expect in the request and use the validator
crate to enforce validation rules.
First, define the struct in main.rs
:
use axum::{
extract::{Json, Path},
http::StatusCode,
routing::post,
Router,
};
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Deserialize, Validate)]
struct User {
#[validate(length(min = 1))]
name: String,
#[validate(email)]
email: String,
}
async fn create_user(Json(payload): Json<User>) -> Result<String, StatusCode> {
// Validate the incoming data
if let Err(_) = payload.validate() {
return Err(StatusCode::BAD_REQUEST);
}
Ok(format!("User {} created!", payload.name))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/user", post(create_user));
axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
Explanation:
-
User Struct: We’ve created a struct
User
withname
andemail
fields. The#[validate(...)]
attributes specify validation rules:length(min = 1)
ensures thename
field is not empty.email
ensures theemail
field is a valid email address.
-
create_user Function: The
create_user
handler extracts theUser
object from the JSON payload and validates it usingpayload.validate()
. If the validation fails, it returns aBAD_REQUEST
(400) status code. If the data is valid, it sends a success message.
Step 4: Data Sanitization
Next, we need to ensure that any potentially unsafe input is sanitized. This step helps to prevent vulnerabilities like XSS. In this example, we'll sanitize the name
field to remove any harmful HTML.
Add a simple sanitization function in your main.rs
file:
use ammonia::clean;
fn sanitize_input(input: &str) -> String {
clean(input).to_string()
}
Now, in the create_user
function, sanitize the name before sending a response:
async fn create_user(Json(payload): Json<User>) -> Result<String, StatusCode> {
// Validate the incoming data
if let Err(_) = payload.validate() {
return Err(StatusCode::BAD_REQUEST);
}
let sanitized_name = sanitize_input(&payload.name);
Ok(format!("User {} created!", sanitized_name))
}
Explanation:
- Sanitize Function: We use the
ammonia
crate to sanitize thename
input, ensuring that any malicious HTML or JavaScript is removed. This prevents XSS attacks.
Challenges or Questions
Now that you've seen how to validate and sanitize data, try the following:
- Validation Challenge: Modify the
User
struct to include aage
field. Ensure thatage
is an integer and falls within a valid range (e.g., between 18 and 100). - Sanitization Challenge: Use a different sanitization method or library and observe how it changes the sanitization process. Can you handle other types of malicious data, such as SQL injection strings?
Recap and Conclusion
In this tutorial, we’ve covered:
- Request Validation: Using the
validator
crate to ensure that incoming data adheres to the rules we define (like checking for a valid email or a non-empty name). - Data Sanitization: Using the
ammonia
crate to clean user input and prevent XSS attacks by removing unsafe HTML content. - Axum Setup: We set up a simple Axum server and built out a POST endpoint that handles user data securely.
By applying both validation and sanitization in your Axum application, you’ve taken important steps toward building a robust and secure backend.
To dive deeper, you could explore more advanced validation techniques, or experiment with different Axum features like middleware and error handling.