v0.24.5
This commit is contained in:
commit
5e23e9af02
240 files changed
+99957
No files matched your search
+145
@@ -0,0 +1,145 @@
|
||||
//! Explicit per-vhost response protection. HSTS is never emitted on plaintext.
|
||||
use anyhow::{ensure, Result};
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct SecurityHeaders {
|
||||
pub hsts: Hsts,
|
||||
pub nosniff: bool,
|
||||
pub frame_options: FrameOptions,
|
||||
pub referrer_policy: Option<String>,
|
||||
pub content_security_policy: Option<String>,
|
||||
pub permissions_policy: Option<String>,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Hsts {
|
||||
pub enabled: bool,
|
||||
pub max_age_seconds: u64,
|
||||
pub include_subdomains: bool,
|
||||
pub preload: bool,
|
||||
}
|
||||
impl Default for Hsts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
max_age_seconds: 31_536_000,
|
||||
include_subdomains: false,
|
||||
preload: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FrameOptions {
|
||||
#[default]
|
||||
Preserve,
|
||||
Deny,
|
||||
Sameorigin,
|
||||
}
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CompiledSecurity {
|
||||
hsts: Option<HeaderValue>,
|
||||
headers: Vec<(&'static str, HeaderValue)>,
|
||||
}
|
||||
impl SecurityHeaders {
|
||||
pub fn compile(&self) -> Result<CompiledSecurity> {
|
||||
ensure!(
|
||||
self.hsts.max_age_seconds <= 63_072_000,
|
||||
"HSTS max_age_seconds must be 0..63072000"
|
||||
);
|
||||
ensure!(
|
||||
!self.hsts.preload
|
||||
|| (self.hsts.enabled
|
||||
&& self.hsts.include_subdomains
|
||||
&& self.hsts.max_age_seconds >= 31_536_000),
|
||||
"HSTS preload requires enabled, include_subdomains and at least one year"
|
||||
);
|
||||
let mut result = CompiledSecurity::default();
|
||||
if self.hsts.enabled {
|
||||
let mut value = format!("max-age={}", self.hsts.max_age_seconds);
|
||||
if self.hsts.include_subdomains {
|
||||
value.push_str("; includeSubDomains");
|
||||
}
|
||||
if self.hsts.preload {
|
||||
value.push_str("; preload");
|
||||
}
|
||||
result.hsts = Some(value.parse()?);
|
||||
}
|
||||
if self.nosniff {
|
||||
result.headers.push((
|
||||
"x-content-type-options",
|
||||
HeaderValue::from_static("nosniff"),
|
||||
));
|
||||
}
|
||||
let frame = match self.frame_options {
|
||||
FrameOptions::Preserve => None,
|
||||
FrameOptions::Deny => Some("DENY"),
|
||||
FrameOptions::Sameorigin => Some("SAMEORIGIN"),
|
||||
};
|
||||
if let Some(value) = frame {
|
||||
result
|
||||
.headers
|
||||
.push(("x-frame-options", HeaderValue::from_static(value)));
|
||||
}
|
||||
for (name, value) in [
|
||||
("referrer-policy", &self.referrer_policy),
|
||||
("content-security-policy", &self.content_security_policy),
|
||||
("permissions-policy", &self.permissions_policy),
|
||||
] {
|
||||
if let Some(value) = value {
|
||||
ensure!(
|
||||
!value.is_empty() && value.len() <= 8192,
|
||||
"security header must contain 1..8192 bytes"
|
||||
);
|
||||
result.headers.push((name, value.parse()?));
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
impl CompiledSecurity {
|
||||
pub fn apply(&self, headers: &mut HeaderMap, tls: bool) {
|
||||
for (name, value) in &self.headers {
|
||||
headers.insert(*name, value.clone());
|
||||
}
|
||||
if !tls {
|
||||
headers.remove("strict-transport-security");
|
||||
} else if let Some(value) = &self.hsts {
|
||||
headers.insert("strict-transport-security", value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn hsts_is_tls_only_and_preload_is_explicit() {
|
||||
let mut s = SecurityHeaders::default();
|
||||
s.hsts.enabled = true;
|
||||
let c = s.compile().unwrap();
|
||||
let mut h = HeaderMap::new();
|
||||
c.apply(&mut h, true);
|
||||
assert_eq!(h["strict-transport-security"], "max-age=31536000");
|
||||
c.apply(&mut h, false);
|
||||
assert!(!h.contains_key("strict-transport-security"));
|
||||
s.hsts.preload = true;
|
||||
assert!(s.compile().is_err());
|
||||
s.hsts.include_subdomains = true;
|
||||
assert!(s.compile().is_ok());
|
||||
s.hsts.max_age_seconds = 0;
|
||||
assert!(s.compile().is_err());
|
||||
s.hsts.preload = false;
|
||||
assert!(s.compile().is_ok());
|
||||
}
|
||||
#[test]
|
||||
fn rejects_response_splitting() {
|
||||
let s = SecurityHeaders {
|
||||
content_security_policy: Some("x\r\ninjected: yes".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(s.compile().is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user