1use crate::ast::Directive;
16use crate::source_list::{Keyword, SourceExpression, SourceList, parse_source_list};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum ValueGrammar {
22 SourceList,
24 AncestorSourceList,
29 SandboxTokens,
31 Boolean,
33 Token,
35 TokenList,
41 TrustedTypes,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49#[non_exhaustive]
50pub enum DirectiveStatus {
51 Current,
53 Deprecated,
57}
58
59const REGISTRY: &[(&str, ValueGrammar, DirectiveStatus)] = &[
60 (
62 "child-src",
63 ValueGrammar::SourceList,
64 DirectiveStatus::Current,
65 ),
66 (
67 "connect-src",
68 ValueGrammar::SourceList,
69 DirectiveStatus::Current,
70 ),
71 (
72 "default-src",
73 ValueGrammar::SourceList,
74 DirectiveStatus::Current,
75 ),
76 (
77 "font-src",
78 ValueGrammar::SourceList,
79 DirectiveStatus::Current,
80 ),
81 (
82 "frame-src",
83 ValueGrammar::SourceList,
84 DirectiveStatus::Current,
85 ),
86 (
87 "img-src",
88 ValueGrammar::SourceList,
89 DirectiveStatus::Current,
90 ),
91 (
92 "manifest-src",
93 ValueGrammar::SourceList,
94 DirectiveStatus::Current,
95 ),
96 (
97 "media-src",
98 ValueGrammar::SourceList,
99 DirectiveStatus::Current,
100 ),
101 (
102 "object-src",
103 ValueGrammar::SourceList,
104 DirectiveStatus::Current,
105 ),
106 (
107 "script-src",
108 ValueGrammar::SourceList,
109 DirectiveStatus::Current,
110 ),
111 (
112 "script-src-elem",
113 ValueGrammar::SourceList,
114 DirectiveStatus::Current,
115 ),
116 (
117 "script-src-attr",
118 ValueGrammar::SourceList,
119 DirectiveStatus::Current,
120 ),
121 (
122 "style-src",
123 ValueGrammar::SourceList,
124 DirectiveStatus::Current,
125 ),
126 (
127 "style-src-elem",
128 ValueGrammar::SourceList,
129 DirectiveStatus::Current,
130 ),
131 (
132 "style-src-attr",
133 ValueGrammar::SourceList,
134 DirectiveStatus::Current,
135 ),
136 (
137 "worker-src",
138 ValueGrammar::SourceList,
139 DirectiveStatus::Current,
140 ),
141 (
143 "base-uri",
144 ValueGrammar::SourceList,
145 DirectiveStatus::Current,
146 ),
147 (
148 "sandbox",
149 ValueGrammar::SandboxTokens,
150 DirectiveStatus::Current,
151 ),
152 (
154 "form-action",
155 ValueGrammar::SourceList,
156 DirectiveStatus::Current,
157 ),
158 (
159 "frame-ancestors",
160 ValueGrammar::AncestorSourceList,
161 DirectiveStatus::Current,
162 ),
163 ("report-to", ValueGrammar::Token, DirectiveStatus::Current),
165 (
166 "report-uri",
167 ValueGrammar::TokenList,
168 DirectiveStatus::Deprecated,
169 ),
170 (
172 "upgrade-insecure-requests",
173 ValueGrammar::Boolean,
174 DirectiveStatus::Current,
175 ),
176 (
177 "block-all-mixed-content",
178 ValueGrammar::Boolean,
179 DirectiveStatus::Deprecated,
180 ),
181 (
183 "require-trusted-types-for",
184 ValueGrammar::Token,
185 DirectiveStatus::Current,
186 ),
187 (
188 "trusted-types",
189 ValueGrammar::TrustedTypes,
190 DirectiveStatus::Current,
191 ),
192 (
194 "plugin-types",
195 ValueGrammar::TokenList,
196 DirectiveStatus::Deprecated,
197 ),
198];
199
200pub fn registry_lookup(name: &str) -> Option<(ValueGrammar, DirectiveStatus)> {
206 REGISTRY
207 .iter()
208 .find(|(registered_name, _, _)| registered_name.eq_ignore_ascii_case(name))
209 .map(|(_, grammar, status)| (*grammar, *status))
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
216#[non_exhaustive]
217pub enum DirectiveValue {
218 SourceList(SourceList),
220 AncestorSourceList(SourceList),
222 Sandbox(Vec<String>),
224 Boolean,
226 Token(Option<String>),
228 TokenList(Vec<String>),
230 TrustedTypes(Vec<String>),
232 Unknown,
235}
236
237impl Directive {
238 pub fn value(&self) -> DirectiveValue {
244 let Some((grammar, _status)) = registry_lookup(&self.name) else {
245 return DirectiveValue::Unknown;
246 };
247 let raw = self.raw_value.as_deref().unwrap_or("");
248 match grammar {
249 ValueGrammar::SourceList => DirectiveValue::SourceList(parse_source_list(raw)),
250 ValueGrammar::AncestorSourceList => {
251 DirectiveValue::AncestorSourceList(parse_source_list(raw))
252 }
253 ValueGrammar::SandboxTokens => DirectiveValue::Sandbox(tokenize(raw)),
254 ValueGrammar::Boolean => DirectiveValue::Boolean,
255 ValueGrammar::Token => {
256 DirectiveValue::Token(raw.split_ascii_whitespace().next().map(str::to_string))
257 }
258 ValueGrammar::TokenList => DirectiveValue::TokenList(tokenize(raw)),
259 ValueGrammar::TrustedTypes => DirectiveValue::TrustedTypes(tokenize(raw)),
260 }
261 }
262
263 pub fn boolean_value_is_unexpected(&self) -> bool {
270 matches!(
271 registry_lookup(&self.name),
272 Some((ValueGrammar::Boolean, _))
273 ) && self.raw_value.is_some()
274 }
275}
276
277fn tokenize(raw: &str) -> Vec<String> {
278 raw.split_ascii_whitespace().map(str::to_string).collect()
279}
280
281pub fn ancestor_source_list_is_valid(list: &SourceList) -> bool {
287 match list {
288 SourceList::None => true,
289 SourceList::Sources(entries) => entries.iter().all(|entry| {
290 matches!(
291 entry.expression,
292 Some(SourceExpression::Scheme(_))
293 | Some(SourceExpression::Host(_))
294 | Some(SourceExpression::Keyword(Keyword::SelfKeyword))
295 )
296 }),
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use crate::parse_policy_list;
304
305 fn directive_value(policy_str: &str) -> DirectiveValue {
306 parse_policy_list(policy_str).policies[0].directives[0].value()
307 }
308
309 #[test]
310 fn fetch_directive_is_source_list() {
311 assert!(matches!(
312 directive_value("default-src 'self'"),
313 DirectiveValue::SourceList(_)
314 ));
315 }
316
317 #[test]
318 fn sandbox_tokens() {
319 assert_eq!(
320 directive_value("sandbox allow-scripts allow-forms"),
321 DirectiveValue::Sandbox(vec!["allow-scripts".to_string(), "allow-forms".to_string()])
322 );
323 }
324
325 #[test]
326 fn base_uri_and_form_action_are_source_list() {
327 assert!(matches!(
328 directive_value("base-uri 'self'"),
329 DirectiveValue::SourceList(_)
330 ));
331 assert!(matches!(
332 directive_value("form-action 'self'"),
333 DirectiveValue::SourceList(_)
334 ));
335 }
336
337 #[test]
338 fn frame_ancestors_accepts_self_and_hosts_but_rejects_unsafe_inline_nonce_hash() {
339 let allowed = directive_value("frame-ancestors 'self' example.com https:");
340 match allowed {
341 DirectiveValue::AncestorSourceList(list) => {
342 assert!(ancestor_source_list_is_valid(&list));
343 }
344 other => panic!("expected AncestorSourceList, got {other:?}"),
345 }
346
347 for rejected_raw in [
348 "frame-ancestors 'unsafe-inline'",
349 "frame-ancestors 'nonce-abc123'",
350 "frame-ancestors 'sha256-abc123'",
351 ] {
352 match directive_value(rejected_raw) {
353 DirectiveValue::AncestorSourceList(list) => {
354 assert!(!ancestor_source_list_is_valid(&list), "{rejected_raw}");
355 }
356 other => panic!("expected AncestorSourceList, got {other:?}"),
357 }
358 }
359 }
360
361 #[test]
362 fn report_to_and_report_uri() {
363 assert_eq!(
364 directive_value("report-to endpoint-1"),
365 DirectiveValue::Token(Some("endpoint-1".to_string()))
366 );
367 assert_eq!(
368 directive_value("report-uri https://example.com/csp-report"),
369 DirectiveValue::TokenList(vec!["https://example.com/csp-report".to_string()])
370 );
371 assert_eq!(
372 registry_lookup("report-uri").map(|(_, status)| status),
373 Some(DirectiveStatus::Deprecated)
374 );
375 }
376
377 #[test]
378 fn boolean_directives() {
379 let list = parse_policy_list("upgrade-insecure-requests");
380 let directive = &list.policies[0].directives[0];
381 assert_eq!(directive.value(), DirectiveValue::Boolean);
382 assert!(!directive.boolean_value_is_unexpected());
383
384 let list = parse_policy_list("upgrade-insecure-requests 'self'");
385 let directive = &list.policies[0].directives[0];
386 assert_eq!(directive.value(), DirectiveValue::Boolean);
387 assert!(directive.boolean_value_is_unexpected());
388
389 assert_eq!(
390 registry_lookup("block-all-mixed-content").map(|(_, status)| status),
391 Some(DirectiveStatus::Deprecated)
392 );
393 }
394
395 #[test]
396 fn trusted_types_directives() {
397 assert_eq!(
398 directive_value("require-trusted-types-for 'script'"),
399 DirectiveValue::Token(Some("'script'".to_string()))
400 );
401 assert_eq!(
402 directive_value("trusted-types my-policy 'allow-duplicates'"),
403 DirectiveValue::TrustedTypes(vec![
404 "my-policy".to_string(),
405 "'allow-duplicates'".to_string(),
406 ])
407 );
408 }
409
410 #[test]
411 fn plugin_types_is_deprecated_token_list() {
412 assert_eq!(
413 directive_value("plugin-types application/pdf"),
414 DirectiveValue::TokenList(vec!["application/pdf".to_string()])
415 );
416 assert_eq!(
417 registry_lookup("plugin-types").map(|(_, status)| status),
418 Some(DirectiveStatus::Deprecated)
419 );
420 }
421
422 #[test]
423 fn unknown_directive_stays_syntactically_valid_but_unstructured() {
424 let list = parse_policy_list("default-src 'self'; totally-unknown-directive foo");
425 assert_eq!(list.policies[0].directives.len(), 2);
426 assert_eq!(
427 list.policies[0].directives[1].value(),
428 DirectiveValue::Unknown
429 );
430 }
431
432 #[test]
433 fn registry_lookup_is_case_insensitive() {
434 assert!(registry_lookup("Default-Src").is_some());
435 assert!(registry_lookup("DEFAULT-SRC").is_some());
436 }
437}