speccoder_web/
security.rs1use axum::http::{HeaderMap, Method, header};
14
15pub const CONTENT_SECURITY_POLICY: &str = "default-src 'self'; script-src 'self'; \
17 style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; \
18 font-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
19
20#[must_use]
22pub fn token() -> String {
23 let mut bytes = [0u8; 32];
24 if getrandom::fill(&mut bytes).is_err() {
27 return String::new();
28 }
29 bytes.iter().map(|b| format!("{b:02x}")).collect()
30}
31
32pub(crate) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
34 a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
35}
36
37#[must_use]
39pub fn authorised(token: &str, headers: &HeaderMap) -> bool {
40 !token.is_empty()
41 && headers
42 .get(header::AUTHORIZATION)
43 .and_then(|v| v.to_str().ok())
44 .and_then(|v| v.strip_prefix("Bearer "))
45 .is_some_and(|t| constant_time_eq(t.as_bytes(), token.as_bytes()))
46}
47
48#[must_use]
50pub fn hosts(port: u16) -> [String; 3] {
51 [
52 format!("127.0.0.1:{port}"),
53 format!("localhost:{port}"),
54 format!("[::1]:{port}"),
55 ]
56}
57
58#[must_use]
60pub fn host_ok(port: u16, headers: &HeaderMap) -> bool {
61 headers
62 .get(header::HOST)
63 .and_then(|v| v.to_str().ok())
64 .is_some_and(|h| hosts(port).iter().any(|ok| ok == h))
65}
66
67#[must_use]
71pub fn origin_ok(port: u16, method: &Method, headers: &HeaderMap) -> bool {
72 if method == Method::GET || method == Method::HEAD {
73 return true;
74 }
75 match headers.get(header::ORIGIN).and_then(|v| v.to_str().ok()) {
76 None => true,
78 Some(o) => hosts(port).iter().any(|h| o == format!("http://{h}")),
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 fn headers(pairs: &[(header::HeaderName, &str)]) -> HeaderMap {
87 let mut h = HeaderMap::new();
88 for (k, v) in pairs {
89 if let Ok(v) = v.parse() {
90 h.insert(k.clone(), v);
91 }
92 }
93 h
94 }
95
96 #[test]
97 fn the_token_host_and_origin_are_checked() {
98 let t = token();
99 assert_eq!(t.len(), 64);
100 assert!(authorised(
101 &t,
102 &headers(&[(header::AUTHORIZATION, &format!("Bearer {t}"))])
103 ));
104 assert!(!authorised(
105 &t,
106 &headers(&[(header::AUTHORIZATION, "Bearer nope")])
107 ));
108 assert!(!authorised(&t, &HeaderMap::new()));
109 assert!(!authorised(
110 "",
111 &headers(&[(header::AUTHORIZATION, "Bearer ")])
112 ));
113 assert!(host_ok(4321, &headers(&[(header::HOST, "127.0.0.1:4321")])));
114 assert!(host_ok(4321, &headers(&[(header::HOST, "localhost:4321")])));
115 assert!(!host_ok(
116 4321,
117 &headers(&[(header::HOST, "evil.example:4321")])
118 ));
119 assert!(!host_ok(4321, &headers(&[(header::HOST, "127.0.0.1:9")])));
120 assert!(origin_ok(
121 4321,
122 &Method::POST,
123 &headers(&[(header::ORIGIN, "http://127.0.0.1:4321")])
124 ));
125 assert!(!origin_ok(
126 4321,
127 &Method::POST,
128 &headers(&[(header::ORIGIN, "https://evil.example")])
129 ));
130 assert!(origin_ok(
131 4321,
132 &Method::GET,
133 &headers(&[(header::ORIGIN, "https://evil.example")])
134 ));
135 }
136}