Askama - safe
If you have an application where users can type in some data that you later display on web pages, especially pages that can be viewed by other then you have to make sure the data does not contain any syntax that would let one person run code in the browser of the other person.
The standard safeguard is to make sure any potential HTML tag submitted by users is escaped when we display them. To faciliate this Askama escapes all HTML contained in variables.
However, if that HTML comes from some trusted source, for example because your input system verified that no dangerous tages are permitted or you have markdown files and you generate HTML from them before passing to the template, then you want to tell Askama to embed the data as it is without escaping.
You can achieve this by using the safe filter.
[package]
name = "askama-templates"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
askama = "0.15.6"
axum = "0.8.8"
serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.50.0", features = ["full"] }
[dev-dependencies]
headers = "0.4.1"
http-body-util = "0.1.3"
tower = { version = "0.5.3", features = ["util"] }
use askama::Template;
use axum::{
Router,
http::StatusCode,
response::{Html, IntoResponse, Response},
routing::get,
};
use serde::Deserialize;
struct HtmlTemplate<T>(T);
impl<T> IntoResponse for HtmlTemplate<T>
where
T: Template,
{
fn into_response(self) -> Response {
match self.0.render() {
Ok(html) => Html(html).into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to render template. Error: {err}"),
)
.into_response(),
}
}
}
#[derive(Template)]
#[template(path = "main.html")]
struct MainTemplate {
content: String,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Params {
text: String,
}
async fn main_page() -> impl IntoResponse {
let content = String::from("<b>Bold</b>");
let template = MainTemplate { content };
HtmlTemplate(template)
}
fn create_router() -> Router {
Router::new().route("/", get(main_page))
}
#[tokio::main]
async fn main() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
println!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, create_router()).await.unwrap();
}
#[cfg(test)]
mod tests;
#![allow(unused)]
fn main() {
use super::*;
use axum::{
body::Body,
http::{Request, StatusCode},
};
use http_body_util::BodyExt;
use tower::ServiceExt;
#[tokio::test]
async fn test_main() {
let response = create_router()
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let content_type = response.headers().get("content-type").unwrap();
assert_eq!(content_type.to_str().unwrap(), "text/html; charset=utf-8");
let body = response.into_body();
let bytes = body.collect().await.unwrap().to_bytes();
let html = String::from_utf8(bytes.to_vec()).unwrap();
assert!(html.contains(r#"<h1>Safe template</h1>"#));
assert!(html.contains(r#"<span id="plain"><b>Bold</b></span>"#));
assert!(html.contains(r#"<span id="safe"><b>Bold</b></span>"#));
}
}
<h1>Safe template</h1>
<span id="plain">{{content}}</span>
<hr>
<span id="safe">{{content | safe}}</span>