Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 29 additions & 29 deletions configuration.md

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion src/app_config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::webserver::content_security_policy::ContentSecurityPolicy;
use crate::webserver::routing::RoutingConfig;
use anyhow::Context;
use clap::Parser;
Expand Down Expand Up @@ -245,7 +246,8 @@ pub struct AppConfig {

/// Content-Security-Policy header to send to the client.
/// If not set, a default policy allowing scripts from the same origin is used and from jsdelivr.net
pub content_security_policy: Option<String>,
#[serde(default = "default_content_security_policy")]
pub content_security_policy: ContentSecurityPolicy,

/// Whether `sqlpage.fetch` should load trusted certificates from the operating system's certificate store
/// By default, it loads Mozilla's root certificates that are embedded in the `SQLPage` binary, or the ones pointed to by the
Expand Down Expand Up @@ -511,6 +513,10 @@ fn default_compress_responses() -> bool {
true
}

fn default_content_security_policy() -> ContentSecurityPolicy {
ContentSecurityPolicy::default()
}

fn default_system_root_ca_certificates() -> bool {
std::env::var("SSL_CERT_FILE").is_ok_and(|x| !x.is_empty())
|| std::env::var("SSL_CERT_DIR").is_ok_and(|x| !x.is_empty())
Expand Down
3 changes: 0 additions & 3 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,6 @@ impl HeaderContext {
) -> Self {
let mut response = HttpResponseBuilder::new(StatusCode::OK);
response.content_type("text/html; charset=utf-8");
if app_state.config.content_security_policy.is_none() {
response.insert_header(&request_context.content_security_policy);
}
Self {
app_state,
request_context,
Expand Down
103 changes: 85 additions & 18 deletions src/webserver/content_security_policy.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,107 @@
use std::fmt::Display;

use actix_web::http::header::{
HeaderName, HeaderValue, TryIntoHeaderPair, CONTENT_SECURITY_POLICY,
};
use awc::http::header::InvalidHeaderValue;
use rand::random;
use serde::Deserialize;
use std::fmt::{Display, Formatter};

#[derive(Debug, Clone, Copy)]
#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(from = "String")]
pub struct ContentSecurityPolicy {
pub nonce: u64,
value: String,
}

impl ContentSecurityPolicy {
#[must_use]
pub fn is_enabled(&self) -> bool {
!self.value.is_empty()
}

fn new<S: Into<String>>(value: S) -> Self {
Self {
nonce: random(),
value: value.into(),
}
}

#[allow(dead_code)]
fn set_nonce(&mut self, nonce: u64) {
self.nonce = nonce;
}
}

impl Default for ContentSecurityPolicy {
fn default() -> Self {
Self { nonce: random() }
Self::new("script-src 'self' 'nonce-{NONCE}'")
}
}

impl Display for ContentSecurityPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "script-src 'self' 'nonce-{}'", self.nonce)
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let value = self
.value
.replace("{NONCE}", self.nonce.to_string().as_str());

write!(f, "{value}")
}
}

impl From<String> for ContentSecurityPolicy {
fn from(input: String) -> Self {
ContentSecurityPolicy::new(input)
}
}

impl actix_web::http::header::TryIntoHeaderPair for &ContentSecurityPolicy {
impl TryIntoHeaderPair for &ContentSecurityPolicy {
type Error = InvalidHeaderValue;

fn try_into_pair(
self,
) -> Result<
(
actix_web::http::header::HeaderName,
actix_web::http::header::HeaderValue,
),
Self::Error,
> {
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
Ok((
actix_web::http::header::CONTENT_SECURITY_POLICY,
actix_web::http::header::HeaderValue::from_str(&self.to_string())?,
CONTENT_SECURITY_POLICY,
HeaderValue::from_str(&self.to_string())?,
))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn default_csp_contains_random_nonce() {
let mut csp = ContentSecurityPolicy::default();
csp.set_nonce(0);

assert_eq!(csp.to_string().as_str(), "script-src 'self' 'nonce-0'");
assert!(csp.is_enabled());
}

#[test]
fn custom_csp_without_nonce() {
let csp: ContentSecurityPolicy = String::from("object-src 'none';").into();
assert_eq!("object-src 'none';", csp.to_string().as_str());
assert!(csp.is_enabled());
}

#[test]
fn blank_csp() {
let csp: ContentSecurityPolicy = String::from("").into();
assert_eq!("", csp.to_string().as_str());
assert!(!csp.is_enabled());
}

#[test]
fn custom_csp_with_nonce() {
let mut csp: ContentSecurityPolicy =
String::from("script-src 'self' 'nonce-{NONCE}'; object-src 'none';").into();
csp.set_nonce(0);

assert_eq!(
"script-src 'self' 'nonce-0'; object-src 'none';",
csp.to_string().as_str()
);
assert!(csp.is_enabled());
}
}
7 changes: 4 additions & 3 deletions src/webserver/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ async fn render_sql(
actix_web::rt::spawn(async move {
let request_context = RequestContext {
is_embedded: req_param.get_variables.contains_key("_sqlpage_embed"),
content_security_policy: ContentSecurityPolicy::default(),
content_security_policy: app_state.config.content_security_policy.clone(),
};
let mut conn = None;
let database_entries_stream =
Expand Down Expand Up @@ -507,8 +507,9 @@ pub fn payload_config(app_state: &web::Data<AppState>) -> PayloadConfig {
fn default_headers(app_state: &web::Data<AppState>) -> middleware::DefaultHeaders {
let server_header = format!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
let mut headers = middleware::DefaultHeaders::new().add(("Server", server_header));
if let Some(csp) = &app_state.config.content_security_policy {
headers = headers.add(("Content-Security-Policy", csp.as_str()));
let csp = &app_state.config.content_security_policy;
if csp.is_enabled() {
headers = headers.add(csp);
}
headers
}
Expand Down
2 changes: 1 addition & 1 deletion src/webserver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
//! - [`static_content`]: Static asset handling (JS, CSS, icons)
//!

mod content_security_policy;
pub mod content_security_policy;
pub mod database;
pub mod error_with_status;
pub mod http;
Expand Down
Loading