From bb35dd97a23c948d1a4f981a7b2bc85d308ce272 Mon Sep 17 00:00:00 2001 From: neri Date: Fri, 26 Jul 2024 02:09:34 +0200 Subject: [PATCH] feat: better security with + diff --git a/src/main.rs b/src/main.rs index 97eb5b2..11f4e85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod file_info; mod mime_relations; mod multipart; mod rate_limit; +mod script_nonce; mod template; mod upload; @@ -14,31 +15,29 @@ use actix_files::Files; use actix_governor::{Governor, GovernorConfigBuilder}; use actix_web::{ http::header::{ - HeaderName, CONTENT_SECURITY_POLICY, PERMISSIONS_POLICY, REFERRER_POLICY, + HeaderName, CROSS_ORIGIN_OPENER_POLICY, PERMISSIONS_POLICY, REFERRER_POLICY, X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS, X_XSS_PROTECTION, }, middleware::{self, Condition, DefaultHeaders}, web::{self, Data}, App, Error, HttpResponse, HttpServer, }; +use actix_web_lab::middleware::from_fn; use env_logger::Env; use sqlx::postgres::PgPool; use std::env; use tokio::sync::mpsc::channel; -const DEFAULT_CONTENT_SECURITY_POLICY: (HeaderName, &str) = ( - CONTENT_SECURITY_POLICY, - "default-src 'none'; connect-src 'self'; img-src 'self'; media-src 'self'; font-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; base-uri 'self'; frame-src 'none'; frame-ancestors 'none'; form-action 'self';" -); -#[allow(clippy::declare_interior_mutable_const)] -const DEFAULT_PERMISSIONS: (HeaderName, &str) = ( +static DEFAULT_PERMISSIONS: (HeaderName, &str) = ( PERMISSIONS_POLICY, "accelerometer=(), ambient-light-sensor=(), battery=(), camera=(), display-capture=(), document-domain=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=(), usb=(), web-share=()" ); -const DEFAULT_CONTENT_TYPE_OPTIONS: (HeaderName, &str) = (X_CONTENT_TYPE_OPTIONS, "nosniff"); -const DEFAULT_FRAME_OPTIONS: (HeaderName, &str) = (X_FRAME_OPTIONS, "deny"); -const DEFAULT_XSS_PROTECTION: (HeaderName, &str) = (X_XSS_PROTECTION, "1; mode=block"); -const DEFAULT_REFERRER_POLICY: (HeaderName, &str) = (REFERRER_POLICY, "no-referrer"); +static DEFAULT_CONTENT_TYPE_OPTIONS: (HeaderName, &str) = (X_CONTENT_TYPE_OPTIONS, "nosniff"); +static DEFAULT_FRAME_OPTIONS: (HeaderName, &str) = (X_FRAME_OPTIONS, "deny"); +static DEFAULT_XSS_PROTECTION: (HeaderName, &str) = (X_XSS_PROTECTION, "1; mode=block"); +static DEFAULT_REFERRER_POLICY: (HeaderName, &str) = (REFERRER_POLICY, "no-referrer"); +static DEFAULT_CROSS_ORIGIN_OPENER_POLICY: (HeaderName, &str) = + (CROSS_ORIGIN_OPENER_POLICY, "same-origin"); async fn not_found() -> Result { Ok(HttpResponse::NotFound() @@ -85,15 +84,16 @@ async fn main() -> std::io::Result<()> { App::new() .wrap( DefaultHeaders::new() - .add(DEFAULT_CONTENT_SECURITY_POLICY) - .add(DEFAULT_PERMISSIONS) - .add(DEFAULT_CONTENT_TYPE_OPTIONS) - .add(DEFAULT_FRAME_OPTIONS) - .add(DEFAULT_XSS_PROTECTION) - .add(DEFAULT_REFERRER_POLICY), + .add(DEFAULT_PERMISSIONS.clone()) + .add(DEFAULT_CONTENT_TYPE_OPTIONS.clone()) + .add(DEFAULT_FRAME_OPTIONS.clone()) + .add(DEFAULT_XSS_PROTECTION.clone()) + .add(DEFAULT_REFERRER_POLICY.clone()) + .add(DEFAULT_CROSS_ORIGIN_OPENER_POLICY.clone()), ) .wrap(middleware::Compress::default()) .wrap(middleware::NormalizePath::trim()) + .wrap(from_fn(script_nonce::insert_script_nonce)) .app_data(db.clone()) .app_data(expiry_watch_sender.clone()) .app_data(config.clone()) diff --git a/src/script_nonce.rs b/src/script_nonce.rs new file mode 100644 index 0000000..c68e6c9 --- /dev/null +++ b/src/script_nonce.rs @@ -0,0 +1,35 @@ +use actix_web::{ + body::MessageBody, + dev::{ServiceRequest, ServiceResponse}, + http::header::{HeaderValue, CONTENT_SECURITY_POLICY}, + Error, HttpMessage, +}; +use actix_web_lab::middleware::Next; +use rand::Rng; +use std::fmt::Display; + +pub struct ScriptNonce(String); + +impl Display for ScriptNonce { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +pub async fn insert_script_nonce( + req: ServiceRequest, + next: Next, +) -> Result, Error> { + let script_nonce = format!("{:02x}", rand::thread_rng().gen::()); + req.extensions_mut() + .insert(ScriptNonce(script_nonce.clone())); + let mut res = next.call(req).await; + if let Ok(res) = res.as_mut() { + let value = format!("default-src 'none'; connect-src 'self'; img-src 'self'; media-src 'self'; font-src 'self'; script-src 'nonce-{script_nonce}'; style-src 'self'; object-src 'none'; base-uri 'self'; frame-src 'none'; frame-ancestors 'none'; form-action 'self'; require-trusted-types-for 'script';"); + res.headers_mut().insert( + CONTENT_SECURITY_POLICY, + HeaderValue::from_str(&value).unwrap(), + ); + } + res +} diff --git a/src/template.rs b/src/template.rs index 28b8fc5..558ac27 100644 --- a/src/template.rs +++ b/src/template.rs @@ -1,11 +1,11 @@ use std::{cmp, io::ErrorKind, str::FromStr}; -use actix_web::HttpRequest; +use actix_web::{HttpMessage, HttpRequest}; use time::Duration; use tokio::fs; use url::Url; -use crate::config::Config; +use crate::{config::Config, script_nonce::ScriptNonce}; const INDEX_HTML: &str = include_str!("../template/index.html"); const AUTH_HIDE_JS: &str = include_str!("../template/auth-hide.js"); @@ -34,7 +34,8 @@ pub fn build_uploaded_html( } else { UPLOAD_HTML.replace("{link}", &get_file_url(req, id, name)) }; - insert_abuse_template(upload_html, None, config) + let upload_html = insert_abuse_template(upload_html, None, config); + insert_script_nonce(req, upload_html) } pub fn get_file_url(req: &HttpRequest, id: &str, name: Option<&str>) -> String { @@ -68,15 +69,11 @@ pub fn build_html_view_template( .replace("{file_name}", &name_snippet) .replace("{text}", &encoded_content) }; - insert_abuse_template(html, Some(req), config) + let html = insert_abuse_template(html, Some(req), config); + insert_script_nonce(req, html) } pub async fn write_prefillable_templates(config: &Config) { - let index_path = config.static_dir.join("index.html"); - fs::write(index_path, build_index_html(config)) - .await - .expect("could not write index.html to static folder"); - let auth_hide_path = config.static_dir.join("auth-hide.js"); if let Some(auth_hide_js) = build_auth_hide_js(config) { fs::write(auth_hide_path, auth_hide_js) @@ -90,7 +87,7 @@ pub async fn write_prefillable_templates(config: &Config) { } } -fn build_index_html(config: &Config) -> String { +pub fn build_index_html(req: &HttpRequest, config: &Config) -> String { let mut html = INDEX_HTML.to_owned(); if let Some(limit) = config.no_auth_limits.as_ref() { html = html @@ -115,7 +112,7 @@ fn build_index_html(config: &Config) -> String { } else { html = html.replace("{max_size_snippet}", ""); }; - html + insert_script_nonce(req, html) } pub fn insert_abuse_template(html: String, req: Option<&HttpRequest>, config: &Config) -> String { @@ -134,6 +131,12 @@ pub fn insert_abuse_template(html: String, req: Option<&HttpRequest>, config: &C } } +pub fn insert_script_nonce(req: &HttpRequest, html: String) -> String { + let extensions = &req.extensions(); + let script_nonce = extensions.get::().expect("script_nonce available"); + html.replace("{script_nonce}", &script_nonce.to_string()) +} + fn render_file_size(size: u64) -> String { let magnitude = cmp::min((size as f64).log(1024.0) as u32, 5); let prefix = ["", "ki", "Mi", "Gi", "Ti", "Pi"][magnitude as usize]; diff --git a/src/upload.rs b/src/upload.rs index e9242dc..d5f2447 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -3,7 +3,6 @@ use std::io::ErrorKind; use crate::config::Config; use crate::file_info::FileInfo; use crate::{file_info, multipart, template}; -use actix_files::NamedFile; use actix_multipart::Multipart; use actix_web::http::header::LOCATION; use actix_web::{error, web, Error, HttpRequest, HttpResponse}; @@ -18,12 +17,11 @@ const ID_CHARS: &[char] = &[ 'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9', ]; -pub async fn index(config: web::Data) -> Result { - let file = NamedFile::open(config.static_dir.join("index.html")).map_err(|file_err| { - log::error!("index.html could not be read {:?}", file_err); - error::ErrorInternalServerError("this file should be here but could not be found") - })?; - Ok(file.disable_content_disposition()) +pub async fn index(req: HttpRequest, config: web::Data) -> HttpResponse { + let index_html = template::build_index_html(&req, &config); + HttpResponse::Ok() + .content_type("text/html") + .body(index_html) } pub async fn upload( diff --git a/template/index.html b/template/index.html index 9f33f1c..ecf97e3 100644 --- a/template/index.html +++ b/template/index.html @@ -93,7 +93,7 @@ repo - - + + diff --git a/template/text-view.html b/template/text-view.html index 6ff0c55..bb0503c 100644 --- a/template/text-view.html +++ b/template/text-view.html @@ -36,6 +36,6 @@ repo - + diff --git a/template/upload-short.html b/template/upload-short.html index 779184d..18cf1fe 100644 --- a/template/upload-short.html +++ b/template/upload-short.html @@ -36,6 +36,6 @@ repo - + diff --git a/template/upload.html b/template/upload.html index adb7562..0a4ba16 100644 --- a/template/upload.html +++ b/template/upload.html @@ -32,6 +32,6 @@ repo - + diff --git a/template/url-view.html b/template/url-view.html index 4479ad6..549dafd 100644 --- a/template/url-view.html +++ b/template/url-view.html @@ -44,6 +44,6 @@ repo - +