Skip to content

Commit 94a56bf

Browse files
committed
more memory efficient CSP templates
1 parent 06292a9 commit 94a56bf

File tree

1 file changed

+41
-57
lines changed

1 file changed

+41
-57
lines changed

src/webserver/content_security_policy.rs

Lines changed: 41 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
1-
use actix_web::http::header::{
2-
HeaderName, HeaderValue, TryIntoHeaderPair, CONTENT_SECURITY_POLICY,
3-
};
1+
use actix_web::http::header::CONTENT_SECURITY_POLICY;
42
use actix_web::HttpResponseBuilder;
5-
use awc::http::header::InvalidHeaderValue;
63
use rand::random;
74
use serde::Deserialize;
8-
use std::fmt::{Display, Formatter};
9-
use std::sync::Arc;
105

116
pub const DEFAULT_CONTENT_SECURITY_POLICY: &str = "script-src 'self' 'nonce-{NONCE}'";
7+
pub const NONCE_PLACEHOLDER: &str = "{NONCE}";
128

139
#[derive(Debug, Clone)]
1410
pub struct ContentSecurityPolicy {
1511
pub nonce: u64,
16-
template: ContentSecurityPolicyTemplate,
1712
}
1813

1914
/// A template for the Content Security Policy header.
@@ -22,8 +17,28 @@ pub struct ContentSecurityPolicy {
2217
/// This struct is cheap to clone.
2318
#[derive(Debug, Clone, PartialEq, Eq)]
2419
pub struct ContentSecurityPolicyTemplate {
25-
pub before_nonce: Arc<str>,
26-
pub after_nonce: Option<Arc<str>>,
20+
pub template: String,
21+
pub nonce_position: Option<usize>,
22+
}
23+
24+
impl ContentSecurityPolicyTemplate {
25+
#[must_use]
26+
pub fn is_enabled(&self) -> bool {
27+
self.nonce_position.is_some()
28+
}
29+
30+
fn format_nonce(&self, nonce: u64) -> String {
31+
if let Some(pos) = self.nonce_position {
32+
format!(
33+
"{}{}{}",
34+
&self.template[..pos],
35+
nonce,
36+
&self.template[pos + NONCE_PLACEHOLDER.len()..]
37+
)
38+
} else {
39+
self.template.clone()
40+
}
41+
}
2742
}
2843

2944
impl Default for ContentSecurityPolicyTemplate {
@@ -34,16 +49,10 @@ impl Default for ContentSecurityPolicyTemplate {
3449

3550
impl From<&str> for ContentSecurityPolicyTemplate {
3651
fn from(s: &str) -> Self {
37-
if let Some((before, after)) = s.split_once("{NONCE}") {
38-
Self {
39-
before_nonce: Arc::from(before),
40-
after_nonce: Some(Arc::from(after)),
41-
}
42-
} else {
43-
Self {
44-
before_nonce: Arc::from(s),
45-
after_nonce: None,
46-
}
52+
let nonce_position = s.find(NONCE_PLACEHOLDER);
53+
Self {
54+
template: s.to_owned(),
55+
nonce_position,
4756
}
4857
}
4958
}
@@ -60,44 +69,19 @@ impl<'de> Deserialize<'de> for ContentSecurityPolicyTemplate {
6069

6170
impl ContentSecurityPolicy {
6271
#[must_use]
63-
pub fn new(template: ContentSecurityPolicyTemplate) -> Self {
64-
Self {
65-
nonce: random(),
66-
template,
67-
}
72+
pub fn with_random_nonce() -> Self {
73+
Self { nonce: random() }
6874
}
6975

70-
pub fn apply_to_response(&self, response: &mut HttpResponseBuilder) {
71-
if self.is_enabled() {
72-
response.insert_header(self);
76+
pub fn apply_to_response(
77+
&self,
78+
template: &ContentSecurityPolicyTemplate,
79+
response: &mut HttpResponseBuilder,
80+
) {
81+
if template.is_enabled() {
82+
response.insert_header((CONTENT_SECURITY_POLICY, template.format_nonce(self.nonce)));
7383
}
7484
}
75-
76-
fn is_enabled(&self) -> bool {
77-
!self.template.before_nonce.is_empty() || self.template.after_nonce.is_some()
78-
}
79-
}
80-
81-
impl Display for ContentSecurityPolicy {
82-
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
83-
let before = self.template.before_nonce.as_ref();
84-
if let Some(after) = &self.template.after_nonce {
85-
let nonce = self.nonce;
86-
write!(f, "{before}{nonce}{after}")
87-
} else {
88-
write!(f, "{before}")
89-
}
90-
}
91-
}
92-
impl TryIntoHeaderPair for &ContentSecurityPolicy {
93-
type Error = InvalidHeaderValue;
94-
95-
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
96-
Ok((
97-
CONTENT_SECURITY_POLICY,
98-
HeaderValue::from_maybe_shared(self.to_string())?,
99-
))
100-
}
10185
}
10286

10387
#[cfg(test)]
@@ -109,12 +93,12 @@ mod tests {
10993
let template = ContentSecurityPolicyTemplate::from(
11094
"script-src 'self' 'nonce-{NONCE}' 'unsafe-inline'",
11195
);
112-
let csp = ContentSecurityPolicy::new(template.clone());
113-
let csp_str = csp.to_string();
96+
let csp = ContentSecurityPolicy::with_random_nonce();
97+
let csp_str = template.format_nonce(csp.nonce);
11498
assert!(csp_str.starts_with("script-src 'self' 'nonce-"));
11599
assert!(csp_str.ends_with("' 'unsafe-inline'"));
116-
let second_csp = ContentSecurityPolicy::new(template);
117-
let second_csp_str = second_csp.to_string();
100+
let second_csp = ContentSecurityPolicy::with_random_nonce();
101+
let second_csp_str = template.format_nonce(second_csp.nonce);
118102
assert_ne!(
119103
csp_str, second_csp_str,
120104
"We should not generate the same nonce twice"

0 commit comments

Comments
 (0)