From ad1fb8a9afd99fcecb75eb76f09fa8cbbeb18875 Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 7 Jul 2025 06:25:04 +0900 Subject: [PATCH] csrf and xss fix thanks @JorianWoltjer <3 --- src/web/image_proxy.rs | 24 +++++++++++++++++------- src/web/settings.rs | 22 ++++++++++++++++++---- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/web/image_proxy.rs b/src/web/image_proxy.rs index 9b8d668..c0eff9e 100644 --- a/src/web/image_proxy.rs +++ b/src/web/image_proxy.rs @@ -6,6 +6,7 @@ use axum::{ response::{IntoResponse, Response}, Extension, }; +use reqwest::header; use tracing::error; use crate::{config::Config, engines}; @@ -42,6 +43,9 @@ pub async fn route( if res.content_length().unwrap_or_default() > max_size { return (StatusCode::PAYLOAD_TOO_LARGE, "Image too large").into_response(); } + + const ALLOWED_IMAGE_TYPES: &[&str] = &["apng", "avif", "gif", "jpeg", "png", "webp"]; + // validate content-type let content_type = res .headers() @@ -49,8 +53,15 @@ pub async fn route( .and_then(|v| v.to_str().ok()) .unwrap_or_default() .to_string(); - if !content_type.starts_with("image/") { - return (StatusCode::BAD_REQUEST, "Not an image").into_response(); + + let Some((base_type, subtype)) = content_type.split_once("/") else { + return (StatusCode::UNSUPPORTED_MEDIA_TYPE, "Invalid Content-Type").into_response(); + }; + if base_type != "image" { + return (StatusCode::UNSUPPORTED_MEDIA_TYPE, "Not an image").into_response(); + } + if !ALLOWED_IMAGE_TYPES.contains(&subtype) { + return (StatusCode::UNSUPPORTED_MEDIA_TYPE, "Image type not allowed").into_response(); } let mut image_bytes = Vec::new(); @@ -63,11 +74,10 @@ pub async fn route( ( [ - (axum::http::header::CONTENT_TYPE, content_type), - ( - axum::http::header::CACHE_CONTROL, - "public, max-age=31536000".to_owned(), - ), + (header::CONTENT_TYPE, content_type), + (header::CACHE_CONTROL, "public, max-age=31536000".to_owned()), + (header::X_CONTENT_TYPE_OPTIONS, "nosniff".to_owned()), + (header::CONTENT_DISPOSITION, "attachment".to_owned()), ], image_bytes, ) diff --git a/src/web/settings.rs b/src/web/settings.rs index c7fc675..5e93538 100644 --- a/src/web/settings.rs +++ b/src/web/settings.rs @@ -1,6 +1,6 @@ use axum::{ - http::{header, StatusCode}, - response::IntoResponse, + http::{header, HeaderMap, StatusCode}, + response::{IntoResponse, Response}, Extension, Form, }; use axum_extra::extract::{cookie::Cookie, CookieJar}; @@ -69,10 +69,24 @@ pub struct Settings { pub stylesheet_str: String, } -pub async fn post(mut jar: CookieJar, Form(settings): Form) -> impl IntoResponse { +pub async fn post( + headers: HeaderMap, + mut jar: CookieJar, + Form(settings): Form, +) -> Response { + let Some(origin) = headers.get("origin").and_then(|h| h.to_str().ok()) else { + return (StatusCode::BAD_REQUEST, "Missing or invalid Origin header").into_response(); + }; + let Some(host) = headers.get("host").and_then(|h| h.to_str().ok()) else { + return (StatusCode::BAD_REQUEST, "Missing or invalid Host header").into_response(); + }; + if origin != format!("http://{host}") && origin != format!("https://{host}") { + return (StatusCode::BAD_REQUEST, "Origin does not match Host").into_response(); + } + let mut settings_cookie = Cookie::new("settings", serde_json::to_string(&settings).unwrap()); settings_cookie.make_permanent(); jar = jar.add(settings_cookie); - (StatusCode::FOUND, [(header::LOCATION, "/settings")], jar) + (StatusCode::FOUND, [(header::LOCATION, "/settings")], jar).into_response() }