Skip to content

Commit f08fa57

Browse files
committed
implement a proper csp template struct
1 parent aadce60 commit f08fa57

File tree

2 files changed

+62
-16
lines changed

2 files changed

+62
-16
lines changed

src/webserver/content_security_policy.rs

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,45 @@ use rand::random;
77
use std::fmt::{Display, Formatter};
88
use std::sync::Arc;
99

10-
use crate::AppState;
11-
1210
pub const DEFAULT_CONTENT_SECURITY_POLICY: &str = "script-src 'self' 'nonce-{NONCE}'";
1311

1412
#[derive(Debug, Clone)]
1513
pub struct ContentSecurityPolicy {
1614
pub nonce: u64,
17-
app_state: Arc<AppState>,
15+
template: ContentSecurityPolicyTemplate,
16+
}
17+
18+
/// A template for the Content Security Policy header.
19+
/// The template is a string that contains the nonce placeholder.
20+
/// The nonce placeholder is replaced with the nonce value when the Content Security Policy is applied to a response.
21+
/// This struct is cheap to clone.
22+
#[derive(Debug, Clone)]
23+
pub struct ContentSecurityPolicyTemplate {
24+
pub before_nonce: Arc<str>,
25+
pub after_nonce: Option<Arc<str>>,
26+
}
27+
28+
impl From<&str> for ContentSecurityPolicyTemplate {
29+
fn from(s: &str) -> Self {
30+
if let Some((before, after)) = s.split_once("{NONCE}") {
31+
Self {
32+
before_nonce: Arc::from(before),
33+
after_nonce: Some(Arc::from(after)),
34+
}
35+
} else {
36+
Self {
37+
before_nonce: Arc::from(s),
38+
after_nonce: None,
39+
}
40+
}
41+
}
1842
}
1943

2044
impl ContentSecurityPolicy {
21-
pub fn new(app_state: Arc<AppState>) -> Self {
45+
pub fn new(template: ContentSecurityPolicyTemplate) -> Self {
2246
Self {
2347
nonce: random(),
24-
app_state,
48+
template,
2549
}
2650
}
2751

@@ -31,26 +55,22 @@ impl ContentSecurityPolicy {
3155
}
3256
}
3357

34-
fn template_string(&self) -> &str {
35-
&self.app_state.config.content_security_policy
36-
}
37-
3858
fn is_enabled(&self) -> bool {
39-
!self.app_state.config.content_security_policy.is_empty()
59+
!self.template.before_nonce.is_empty()
4060
}
4161
}
4262

4363
impl Display for ContentSecurityPolicy {
4464
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
45-
let template = self.template_string();
46-
if let Some((before, after)) = template.split_once("{NONCE}") {
47-
write!(f, "{before}{nonce}{after}", nonce = self.nonce)
65+
let before = self.template.before_nonce.as_ref();
66+
if let Some(after) = &self.template.after_nonce {
67+
let nonce = self.nonce;
68+
write!(f, "{before}{nonce}{after}")
4869
} else {
49-
write!(f, "{}", template)
70+
write!(f, "{before}")
5071
}
5172
}
5273
}
53-
5474
impl TryIntoHeaderPair for &ContentSecurityPolicy {
5575
type Error = InvalidHeaderValue;
5676

@@ -61,3 +81,25 @@ impl TryIntoHeaderPair for &ContentSecurityPolicy {
6181
))
6282
}
6383
}
84+
85+
#[cfg(test)]
86+
mod tests {
87+
use super::*;
88+
89+
#[test]
90+
fn test_content_security_policy_display() {
91+
let template = ContentSecurityPolicyTemplate::from(
92+
"script-src 'self' 'nonce-{NONCE}' 'unsafe-inline'",
93+
);
94+
let csp = ContentSecurityPolicy::new(template.clone());
95+
let csp_str = csp.to_string();
96+
assert!(csp_str.starts_with("script-src 'self' 'nonce-"));
97+
assert!(csp_str.ends_with("' 'unsafe-inline'"));
98+
let second_csp = ContentSecurityPolicy::new(template);
99+
let second_csp_str = second_csp.to_string();
100+
assert_ne!(
101+
csp_str, second_csp_str,
102+
"We should not generate the same nonce twice"
103+
);
104+
}
105+
}

src/webserver/http.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,11 @@ async fn render_sql(
177177
actix_web::rt::spawn(async move {
178178
let request_context = RequestContext {
179179
is_embedded: req_param.get_variables.contains_key("_sqlpage_embed"),
180-
content_security_policy: ContentSecurityPolicy::new(Arc::clone(&app_state)),
180+
content_security_policy: ContentSecurityPolicy::new(
181+
crate::webserver::content_security_policy::ContentSecurityPolicyTemplate::from(
182+
app_state.config.content_security_policy.as_str(),
183+
),
184+
),
181185
};
182186
let mut conn = None;
183187
let database_entries_stream =

0 commit comments

Comments
 (0)