changes in headers
This commit is contained in:
Generated
+1
-1
@@ -2581,7 +2581,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustpad"
|
||||
version = "0.2.18"
|
||||
version = "0.2.19"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"aws-config",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustpad"
|
||||
version = "0.2.18"
|
||||
version = "0.2.19"
|
||||
edition = "2024"
|
||||
rust-version = "1.94"
|
||||
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
||||
|
||||
+146
-36
@@ -12,7 +12,7 @@ mod pages;
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{DefaultBodyLimit, Request},
|
||||
http::{HeaderName, HeaderValue, Method, StatusCode, header},
|
||||
http::{HeaderMap, HeaderValue, Method, StatusCode, header},
|
||||
middleware::{self, Next},
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
@@ -212,21 +212,9 @@ pub fn router(
|
||||
.layer(DefaultBodyLimit::max(
|
||||
upload_max_size_bytes.saturating_add(1024 * 1024),
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
HeaderName::from_static("x-frame-options"),
|
||||
HeaderValue::from_static("DENY"),
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
HeaderName::from_static("cross-origin-opener-policy"),
|
||||
HeaderValue::from_static("same-origin"),
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
HeaderName::from_static("cross-origin-resource-policy"),
|
||||
HeaderValue::from_static("same-origin"),
|
||||
))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(middleware::from_fn(require_csrf_token))
|
||||
.layer(middleware::from_fn(add_non_asset_security_headers))
|
||||
.layer(middleware::from_fn(apply_response_header_policy))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -248,30 +236,35 @@ async fn require_csrf_token(request: Request, next: Next) -> Response {
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
async fn add_non_asset_security_headers(request: Request, next: Next) -> Response {
|
||||
let path = request.uri().path();
|
||||
|
||||
let is_asset = path.starts_with("/assets/");
|
||||
let is_file = path.starts_with("/f/");
|
||||
let is_icon = matches!(
|
||||
path,
|
||||
"/favicon.svg" | "/favicon.ico" | "/favicon-32.png" | "/apple-touch-icon.png"
|
||||
);
|
||||
|
||||
async fn apply_response_header_policy(request: Request, next: Next) -> Response {
|
||||
let policy = response_header_policy(request.uri().path());
|
||||
let mut response = next.run(request).await;
|
||||
apply_response_headers(policy, response.headers_mut());
|
||||
response
|
||||
}
|
||||
|
||||
if is_file && is_asset && is_icon {
|
||||
let headers = response.headers_mut();
|
||||
headers.remove("x-frame-options");
|
||||
headers.remove("cross-origin-opener-policy");
|
||||
headers.remove("cross-origin-resource-policy");
|
||||
headers.remove("referrer-policy");
|
||||
headers.remove("permissions-policy");
|
||||
} else if !is_asset && !is_icon {
|
||||
let headers = response.headers_mut();
|
||||
fn apply_response_headers(policy: ResponseHeaderPolicy, headers: &mut HeaderMap) {
|
||||
match policy {
|
||||
ResponseHeaderPolicy::StaticAsset => {
|
||||
headers
|
||||
.entry(header::X_CONTENT_TYPE_OPTIONS)
|
||||
.or_insert(HeaderValue::from_static("nosniff"));
|
||||
}
|
||||
ResponseHeaderPolicy::Application => {
|
||||
headers
|
||||
.entry("x-frame-options")
|
||||
.or_insert(HeaderValue::from_static("DENY"));
|
||||
|
||||
headers
|
||||
.entry("x-content-type-options")
|
||||
.entry("cross-origin-opener-policy")
|
||||
.or_insert(HeaderValue::from_static("same-origin"));
|
||||
|
||||
headers
|
||||
.entry("cross-origin-resource-policy")
|
||||
.or_insert(HeaderValue::from_static("same-origin"));
|
||||
|
||||
headers
|
||||
.entry(header::X_CONTENT_TYPE_OPTIONS)
|
||||
.or_insert(HeaderValue::from_static("nosniff"));
|
||||
|
||||
headers
|
||||
@@ -284,6 +277,123 @@ async fn add_non_asset_security_headers(request: Request, next: Next) -> Respons
|
||||
"camera=(), microphone=(), geolocation=(), payment=(), usb=()",
|
||||
));
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum ResponseHeaderPolicy {
|
||||
Application,
|
||||
StaticAsset,
|
||||
}
|
||||
|
||||
fn response_header_policy(path: &str) -> ResponseHeaderPolicy {
|
||||
if is_asset_path(path) || is_icon_path(path) {
|
||||
ResponseHeaderPolicy::StaticAsset
|
||||
} else {
|
||||
ResponseHeaderPolicy::Application
|
||||
}
|
||||
}
|
||||
|
||||
fn is_asset_path(path: &str) -> bool {
|
||||
path == "/assets" || path.starts_with("/assets/")
|
||||
}
|
||||
|
||||
fn is_icon_path(path: &str) -> bool {
|
||||
path == "/icons"
|
||||
|| path.starts_with("/icons/")
|
||||
|| matches!(
|
||||
path,
|
||||
"/favicon.svg" | "/favicon.ico" | "/favicon-32.png" | "/apple-touch-icon.png"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ResponseHeaderPolicy, apply_response_headers, response_header_policy};
|
||||
use axum::http::{HeaderMap, HeaderValue, header};
|
||||
|
||||
#[test]
|
||||
fn classifies_assets_and_icons_as_static_assets() {
|
||||
for path in [
|
||||
"/assets/app.js",
|
||||
"/assets",
|
||||
"/favicon.ico",
|
||||
"/icons/favicon.svg",
|
||||
"/icons/missing.svg",
|
||||
] {
|
||||
assert_eq!(
|
||||
response_header_policy(path),
|
||||
ResponseHeaderPolicy::StaticAsset
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_file_routes_on_the_application_policy() {
|
||||
for path in [
|
||||
"/f",
|
||||
"/f/token/image.png",
|
||||
"/files",
|
||||
"/files/legacy/image.png",
|
||||
] {
|
||||
assert_eq!(
|
||||
response_header_policy(path),
|
||||
ResponseHeaderPolicy::Application
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_other_routes_as_application() {
|
||||
for path in ["/", "/api/auth/me", "/static/missing.css", "/unknown"] {
|
||||
assert_eq!(
|
||||
response_header_policy(path),
|
||||
ResponseHeaderPolicy::Application
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_asset_policy_only_adds_nosniff() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("public, max-age=3600"),
|
||||
);
|
||||
|
||||
apply_response_headers(ResponseHeaderPolicy::StaticAsset, &mut headers);
|
||||
|
||||
assert_eq!(headers.len(), 2);
|
||||
assert_eq!(headers[header::X_CONTENT_TYPE_OPTIONS], "nosniff");
|
||||
assert!(!headers.contains_key("x-frame-options"));
|
||||
assert!(!headers.contains_key("cross-origin-opener-policy"));
|
||||
assert!(!headers.contains_key("cross-origin-resource-policy"));
|
||||
assert!(!headers.contains_key("referrer-policy"));
|
||||
assert!(!headers.contains_key("permissions-policy"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_policy_preserves_handler_headers() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"content-security-policy",
|
||||
HeaderValue::from_static("default-src 'none'; sandbox"),
|
||||
);
|
||||
|
||||
apply_response_headers(ResponseHeaderPolicy::Application, &mut headers);
|
||||
|
||||
assert_eq!(
|
||||
headers["content-security-policy"],
|
||||
"default-src 'none'; sandbox"
|
||||
);
|
||||
assert_eq!(headers["x-frame-options"], "DENY");
|
||||
assert_eq!(headers["cross-origin-opener-policy"], "same-origin");
|
||||
assert_eq!(headers["cross-origin-resource-policy"], "same-origin");
|
||||
assert_eq!(headers[header::X_CONTENT_TYPE_OPTIONS], "nosniff");
|
||||
assert_eq!(
|
||||
headers["referrer-policy"],
|
||||
"strict-origin-when-cross-origin"
|
||||
);
|
||||
assert!(headers.contains_key("permissions-policy"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user