Skip to main content

forma_server/
csp.rs

1use base64::{engine::general_purpose::STANDARD, Engine as _};
2use ring::rand::{SecureRandom, SystemRandom};
3
4/// Generate a cryptographically random CSP nonce (base64, 44 chars).
5pub fn generate_csp_nonce() -> String {
6    let rng = SystemRandom::new();
7    let mut bytes = [0u8; 32];
8    rng.fill(&mut bytes).expect("RNG failure");
9    STANDARD.encode(bytes)
10}
11
12/// Build a strict Content-Security-Policy header value.
13///
14/// Allows scripts and styles only via nonce. No inline, no eval.
15pub fn build_csp_header(nonce: &str) -> String {
16    format!(
17        "default-src 'none'; \
18         script-src 'nonce-{nonce}' 'self'; \
19         style-src 'nonce-{nonce}' 'self'; \
20         connect-src 'self'; \
21         img-src 'self' data:; \
22         font-src 'self'; \
23         frame-ancestors 'none'; \
24         base-uri 'none'; \
25         form-action 'self'"
26    )
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32
33    #[test]
34    fn nonce_is_unique() {
35        let a = generate_csp_nonce();
36        let b = generate_csp_nonce();
37        assert_ne!(a, b);
38    }
39
40    #[test]
41    fn nonce_is_base64() {
42        let nonce = generate_csp_nonce();
43        assert_eq!(nonce.len(), 44); // 32 bytes -> 44 base64 chars
44        assert!(STANDARD.decode(&nonce).is_ok());
45    }
46
47    #[test]
48    fn csp_contains_nonce() {
49        let nonce = generate_csp_nonce();
50        let csp = build_csp_header(&nonce);
51        assert!(csp.contains(&format!("'nonce-{nonce}'")));
52    }
53
54    #[test]
55    fn csp_is_strict() {
56        let csp = build_csp_header("test");
57        assert!(csp.contains("default-src 'none'"));
58        assert!(!csp.contains("unsafe-inline"));
59        assert!(!csp.contains("unsafe-eval"));
60    }
61}