changes in headers
This commit is contained in:
Generated
+1
-1
@@ -2581,7 +2581,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.2.18"
|
version = "0.2.19"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"aws-config",
|
"aws-config",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.2.18"
|
version = "0.2.19"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.94"
|
rust-version = "1.94"
|
||||||
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
||||||
|
|||||||
+160
-50
@@ -12,7 +12,7 @@ mod pages;
|
|||||||
use axum::{
|
use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
extract::{DefaultBodyLimit, Request},
|
extract::{DefaultBodyLimit, Request},
|
||||||
http::{HeaderName, HeaderValue, Method, StatusCode, header},
|
http::{HeaderMap, HeaderValue, Method, StatusCode, header},
|
||||||
middleware::{self, Next},
|
middleware::{self, Next},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
@@ -212,21 +212,9 @@ pub fn router(
|
|||||||
.layer(DefaultBodyLimit::max(
|
.layer(DefaultBodyLimit::max(
|
||||||
upload_max_size_bytes.saturating_add(1024 * 1024),
|
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(TraceLayer::new_for_http())
|
||||||
.layer(middleware::from_fn(require_csrf_token))
|
.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)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,42 +236,164 @@ async fn require_csrf_token(request: Request, next: Next) -> Response {
|
|||||||
next.run(request).await
|
next.run(request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_non_asset_security_headers(request: Request, next: Next) -> Response {
|
async fn apply_response_header_policy(request: Request, next: Next) -> Response {
|
||||||
let path = request.uri().path();
|
let policy = response_header_policy(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"
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut response = next.run(request).await;
|
let mut response = next.run(request).await;
|
||||||
|
apply_response_headers(policy, response.headers_mut());
|
||||||
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();
|
|
||||||
|
|
||||||
headers
|
|
||||||
.entry("x-content-type-options")
|
|
||||||
.or_insert(HeaderValue::from_static("nosniff"));
|
|
||||||
|
|
||||||
headers
|
|
||||||
.entry("referrer-policy")
|
|
||||||
.or_insert(HeaderValue::from_static("strict-origin-when-cross-origin"));
|
|
||||||
|
|
||||||
headers
|
|
||||||
.entry("permissions-policy")
|
|
||||||
.or_insert(HeaderValue::from_static(
|
|
||||||
"camera=(), microphone=(), geolocation=(), payment=(), usb=()",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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("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
|
||||||
|
.entry("referrer-policy")
|
||||||
|
.or_insert(HeaderValue::from_static("strict-origin-when-cross-origin"));
|
||||||
|
|
||||||
|
headers
|
||||||
|
.entry("permissions-policy")
|
||||||
|
.or_insert(HeaderValue::from_static(
|
||||||
|
"camera=(), microphone=(), geolocation=(), payment=(), usb=()",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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