1#![forbid(unsafe_code)]
40
41pub extern crate percent_encoding;
42pub extern crate url;
43
44pub mod sandboxing_directive;
45pub(crate) mod text_util;
46
47use once_cell::sync::Lazy;
48use regex::Regex;
49use sandboxing_directive::{parse_a_sandboxing_directive, SandboxingFlagSet};
50#[cfg(feature = "serde")]
51use serde::{Deserialize, Serialize};
52use sha2::Digest;
53use std::borrow::{Borrow, Cow};
54use std::cmp;
55use std::collections::HashSet;
56use std::fmt::{self, Display, Formatter};
57use std::str::FromStr;
58use text_util::{
59 ascii_case_insensitive_match, collect_a_sequence_of_non_ascii_white_space_code_points,
60 split_ascii_whitespace, split_commas, strip_leading_and_trailing_ascii_whitespace,
61};
62pub use url::{Origin, Position, Url};
63use MatchResult::DoesNotMatch;
64use MatchResult::Matches;
65
66fn scheme_is_network(scheme: &str) -> bool {
67 scheme == "ftp" || scheme_is_httpx(scheme)
68}
69
70fn scheme_is_httpx(scheme: &str) -> bool {
71 scheme == "http" || scheme == "https"
72}
73
74#[derive(Clone, Debug)]
80#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
81pub struct Policy {
82 pub directive_set: Vec<Directive>,
83 pub disposition: PolicyDisposition,
84 pub source: PolicySource,
85}
86
87impl Display for Policy {
88 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
89 for (i, directive) in self.directive_set.iter().enumerate() {
90 if i != 0 {
91 write!(f, "; ")?;
92 }
93 <Directive as Display>::fmt(directive, f)?;
94 }
95 Ok(())
96 }
97}
98
99impl Policy {
100 pub fn is_valid(&self) -> bool {
101 self.directive_set.iter().all(Directive::is_valid)
102 && self
103 .directive_set
104 .iter()
105 .map(|d| d.name.clone())
106 .collect::<HashSet<_>>()
107 .len()
108 == self.directive_set.len()
109 && !self.directive_set.is_empty()
110 }
111 pub fn parse(serialized: &str, source: PolicySource, disposition: PolicyDisposition) -> Policy {
113 let mut policy = Policy {
114 directive_set: Vec::new(),
115 source,
116 disposition,
117 };
118 for token in serialized.split(';') {
120 let token = strip_leading_and_trailing_ascii_whitespace(token);
121 if token.is_empty() {
122 continue;
123 };
124 let (directive_name, token) =
125 collect_a_sequence_of_non_ascii_white_space_code_points(token);
126 let mut directive_name = directive_name.to_owned();
127 directive_name.make_ascii_lowercase();
128 if policy.contains_a_directive_whose_name_is(&directive_name) {
129 continue;
130 }
131 let directive_value = split_ascii_whitespace(token).map(String::from).collect();
132 policy.directive_set.push(Directive {
133 name: directive_name,
134 value: directive_value,
135 });
136 }
137 policy
138 }
139 pub fn contains_a_directive_whose_name_is(&self, directive_name: &str) -> bool {
140 self.directive_set.iter().any(|d| d.name == directive_name)
141 }
142 pub fn does_request_violate_policy(&self, request: &Request) -> Violates {
144 if request.initiator == Initiator::Prefetch {
145 return self.does_resource_hint_violate_policy(request);
146 }
147
148 let mut violates = Violates::DoesNotViolate;
149 for directive in &self.directive_set {
150 let result = directive.pre_request_check(request, self);
151 if result == CheckResult::Blocked {
152 violates = Violates::Directive(directive.clone());
153 }
154 }
155 violates
156 }
157
158 pub fn does_resource_hint_violate_policy(&self, request: &Request) -> Violates {
160 let default_directive = &self.directive_set.iter().find(|x| x.name == "default-src");
161
162 if default_directive.is_none() {
163 return Violates::DoesNotViolate;
164 }
165
166 for directive in &self.directive_set {
167 let result = directive.pre_request_check(request, self);
168 if result == CheckResult::Allowed {
169 return Violates::DoesNotViolate;
170 }
171 }
172
173 return Violates::Directive(default_directive.unwrap().clone());
174 }
175}
176
177#[derive(Clone, Debug)]
178#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
179pub struct CspList(pub Vec<Policy>);
181
182impl Display for CspList {
183 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
184 for (i, directive) in self.0.iter().enumerate() {
185 if i != 0 {
186 write!(f, ",")?;
187 }
188 <Policy as Display>::fmt(directive, f)?;
189 }
190 Ok(())
191 }
192}
193
194static TRUSTED_POLICY_SOURCE_GRAMMAR: Lazy<Regex> =
196 Lazy::new(|| Regex::new(r#"^[0-9a-zA-Z\-\#=_\/@\.%]+$"#).unwrap());
197
198impl CspList {
199 pub fn is_valid(&self) -> bool {
200 self.0.iter().all(Policy::is_valid)
201 }
202 pub fn contains_a_header_delivered_content_security_policy(&self) -> bool {
204 self.0
205 .iter()
206 .any(|policy| policy.source == PolicySource::Header)
207 }
208 pub fn parse(list: &str, source: PolicySource, disposition: PolicyDisposition) -> CspList {
210 let mut policies = Vec::new();
211 for token in split_commas(list) {
212 let policy = Policy::parse(token, source, disposition);
213 if policy.directive_set.is_empty() {
214 continue;
215 };
216 policies.push(policy)
217 }
218 CspList(policies)
219 }
220 pub fn append(&mut self, mut other: CspList) {
221 self.0.append(&mut other.0)
222 }
223 pub fn push(&mut self, policy: Policy) {
224 self.0.push(policy)
225 }
226 pub fn report_violations_for_request(&self, request: &Request) -> Vec<Violation> {
232 let mut violations = Vec::new();
233 for policy in &self.0 {
234 if policy.disposition == PolicyDisposition::Enforce {
235 continue;
236 };
237 let violates = policy.does_request_violate_policy(request);
238 if let Violates::Directive(directive) = violates {
239 let resource = ViolationResource::Url(request.url.clone());
240 violations.push(Violation {
241 resource,
242 directive: Directive {
243 name: get_the_effective_directive_for_request(request).to_owned(),
244 value: directive.value.clone(),
245 },
246 policy: policy.clone(),
247 });
248 }
249 }
250 violations
251 }
252 pub fn should_request_be_blocked(&self, request: &Request) -> (CheckResult, Vec<Violation>) {
259 let mut result = CheckResult::Allowed;
260 let mut violations = Vec::new();
261 for policy in &self.0 {
262 if policy.disposition == PolicyDisposition::Report {
263 continue;
264 };
265 let violates = policy.does_request_violate_policy(request);
266 if let Violates::Directive(directive) = violates {
267 result = CheckResult::Blocked;
268 let resource = ViolationResource::Url(request.url.clone());
269 violations.push(Violation {
270 resource,
271 directive: Directive {
272 name: get_the_effective_directive_for_request(request).to_owned(),
273 value: directive.value.clone(),
274 },
275 policy: policy.clone(),
276 });
277 }
278 }
279 (result, violations)
280 }
281 pub fn should_response_to_request_be_blocked(
288 &self,
289 request: &Request,
290 response: &Response,
291 ) -> (CheckResult, Vec<Violation>) {
292 let mut result = CheckResult::Allowed;
295 let mut violations = Vec::new();
296 for policy in &self.0 {
298 for directive in &policy.directive_set {
300 if directive.post_request_check(request, response, policy) == CheckResult::Blocked {
302 violations.push(Violation {
305 resource: ViolationResource::Url(request.url.clone()),
306 directive: Directive {
307 name: get_the_effective_directive_for_request(request).to_owned(),
308 value: directive.value.clone(),
309 },
310 policy: policy.clone(),
311 });
312 if policy.disposition == PolicyDisposition::Enforce {
314 result = CheckResult::Blocked;
315 }
316 }
317 }
318 }
319 (result, violations)
320 }
321 pub fn should_elements_inline_type_behavior_be_blocked(
323 &self,
324 element: &Element,
325 type_: InlineCheckType,
326 source: &str,
327 ) -> (CheckResult, Vec<Violation>) {
328 use CheckResult::*;
329 let mut result = Allowed;
330 let mut violations = Vec::new();
331 for policy in &self.0 {
332 for directive in &policy.directive_set {
333 if directive.inline_check(element, type_, policy, source) == Allowed {
334 continue;
335 }
336 let sample = if directive.value.iter().any(|t| &t[..] == "'report-sample'") {
337 let max_length = cmp::min(40, source.len());
338 Some(source[0..max_length].to_owned())
339 } else {
340 None
341 };
342 let violation = Violation {
343 resource: ViolationResource::Inline { sample },
344 directive: Directive {
345 name: get_the_effective_directive_for_inline_checks(type_).to_owned(),
346 value: directive.value.clone(),
347 },
348 policy: policy.clone(),
349 };
350 violations.push(violation);
351 if policy.disposition == PolicyDisposition::Enforce {
352 result = Blocked;
353 }
354 }
355 }
356 (result, violations)
357 }
358 pub fn is_base_allowed_for_document(
365 &self,
366 base: &Url,
367 self_origin: &Origin,
368 ) -> (CheckResult, Vec<Violation>) {
369 use CheckResult::*;
370 let mut violations = Vec::new();
371 for policy in &self.0 {
372 let directive = policy
373 .directive_set
374 .iter()
375 .find(|directive| directive.name == "base-uri");
376 if let Some(directive) = directive {
377 if SourceList(&directive.value)
378 .does_url_match_source_list_in_origin_with_redirect_count(base, &self_origin, 0)
379 == DoesNotMatch
380 {
381 let violation = Violation {
382 directive: directive.clone(),
383 resource: ViolationResource::Inline { sample: None },
384 policy: policy.clone(),
385 };
386 violations.push(violation);
387 if policy.disposition == PolicyDisposition::Enforce {
388 return (Blocked, violations);
389 }
390 }
391 }
392 }
393 return (Allowed, violations);
394 }
395
396 pub fn is_trusted_type_policy_creation_allowed(
403 &self,
404 policy_name: &str,
405 created_policy_names: &[&str],
406 ) -> (CheckResult, Vec<Violation>) {
407 use CheckResult::*;
408 let mut result = Allowed;
410 let mut violations = Vec::new();
411 for policy in &self.0 {
413 let mut create_violation = false;
415 let directive = policy
417 .directive_set
418 .iter()
419 .find(|directive| directive.name == "trusted-types");
420 if let Some(directive) = directive {
422 if directive.value.len() == 1 && directive.value.contains(&"'none'".to_string()) {
424 create_violation = true;
425 }
426 if created_policy_names.contains(&policy_name)
429 && !directive.value.iter().any(|v| v == "'allow-duplicates'")
430 {
431 create_violation = true;
432 }
433 if !(TRUSTED_POLICY_SOURCE_GRAMMAR.is_match(&policy_name)
436 && (directive.value.iter().any(|p| p == policy_name)
437 || directive.value.iter().any(|v| v == "*")))
438 {
439 create_violation = true;
440 }
441 if !create_violation {
443 continue;
444 }
445 let max_length = cmp::min(40, policy_name.len());
446 let sample = policy_name[0..max_length].to_owned();
448 let violation = Violation {
451 directive: directive.clone(),
452 resource: ViolationResource::TrustedTypePolicy {
454 sample,
456 },
457 policy: policy.clone(),
458 };
459 violations.push(violation);
461 if policy.disposition == PolicyDisposition::Enforce {
463 result = Blocked
464 }
465 }
466 }
467 return (result, violations);
468 }
469 pub fn does_sink_type_require_trusted_types(
476 &self,
477 sink_group: &str,
478 include_report_only_policies: bool,
479 ) -> bool {
480 let sink_group = &sink_group.to_owned();
481 for policy in &self.0 {
483 let directive = policy
485 .directive_set
486 .iter()
487 .find(|directive| directive.name == "require-trusted-types-for");
488 if let Some(directive) = directive {
490 if !directive.value.contains(sink_group) {
492 continue;
493 }
494 let enforced = policy.disposition == PolicyDisposition::Enforce;
496 if enforced {
498 return true;
499 }
500 if include_report_only_policies {
502 return true;
503 }
504 }
505 }
506 false
508 }
509 pub fn should_sink_type_mismatch_violation_be_blocked_by_csp(
516 &self,
517 sink: &str,
518 sink_group: &str,
519 source: &str,
520 ) -> (CheckResult, Vec<Violation>) {
521 use CheckResult::*;
522 let sink_group = &sink_group.to_owned();
523 let mut result = Allowed;
525 let mut violations = Vec::new();
526 let mut sample = source;
528 if sink == "Function" {
530 if sample.starts_with("function anonymous") {
532 sample = &sample[18..];
533 } else if sample.starts_with("async function anonymous") {
535 sample = &sample[24..];
536 } else if sample.starts_with("function* anonymous") {
538 sample = &sample[19..];
539 } else if sample.starts_with("async function* anonymous") {
541 sample = &sample[25..];
542 }
543 }
544 for policy in &self.0 {
546 let directive = policy
548 .directive_set
549 .iter()
550 .find(|directive| directive.name == "require-trusted-types-for");
551 let Some(directive) = directive else { continue };
553 if !directive.value.contains(sink_group) {
555 continue;
556 }
557 let mut trimmed_sample: String = sample.into();
559 trimmed_sample.truncate(40);
560 violations.push(Violation {
563 resource: ViolationResource::TrustedTypeSink {
565 sample: sink.to_owned() + "|" + &trimmed_sample,
567 },
568 directive: directive.clone(),
569 policy: policy.clone(),
570 });
571 if policy.disposition == PolicyDisposition::Enforce {
573 result = Blocked
574 }
575 }
576 (result, violations)
578 }
579 pub fn get_sandboxing_flag_set_for_document(&self) -> Option<SandboxingFlagSet> {
581 self.0
584 .iter()
585 .flat_map(|policy| {
586 policy
587 .directive_set
588 .iter()
589 .rev()
591 .find(|directive| directive.name == "sandbox")
594 .and_then(|directive| directive.get_sandboxing_flag_set_for_document(policy))
595 })
596 .next()
598 }
599 pub fn is_js_evaluation_allowed(&self, source: &str) -> (CheckResult, Vec<Violation>) {
601 let mut result = CheckResult::Allowed;
602 let mut violations = Vec::new();
603 for policy in &self.0 {
605 let directive = policy
607 .directive_set
608 .iter()
609 .find(|directive| directive.name == "script-src")
612 .or_else(|| {
615 policy
616 .directive_set
617 .iter()
618 .find(|directive| directive.name == "default-src")
619 });
620 let Some(directive) = directive else { continue };
622 let source_list = SourceList(&directive.value);
623 if source_list.does_a_source_list_allow_js_evaluation() == AllowResult::Allows {
624 continue;
625 }
626 let trusted_types_required =
629 self.does_sink_type_require_trusted_types("'script'", false);
630 if trusted_types_required
633 && directive
634 .value
635 .iter()
636 .any(|t| ascii_case_insensitive_match(&t[..], "'trusted-types-eval'"))
637 {
638 continue;
639 }
640 if directive
643 .value
644 .iter()
645 .any(|t| ascii_case_insensitive_match(&t[..], "'unsafe-eval'"))
646 {
647 continue;
648 }
649 let sample = if directive.value.iter().any(|t| &t[..] == "'report-sample'") {
652 let max_length = cmp::min(40, source.len());
653 Some(source[0..max_length].to_owned())
654 } else {
655 None
656 };
657 violations.push(Violation {
660 resource: ViolationResource::Eval { sample },
662 directive: directive.clone(),
663 policy: policy.clone(),
664 });
665 if policy.disposition == PolicyDisposition::Enforce {
667 result = CheckResult::Blocked
668 }
669 }
670 (result, violations)
671 }
672 pub fn is_wasm_evaluation_allowed(&self) -> (CheckResult, Vec<Violation>) {
674 let mut result = CheckResult::Allowed;
675 let mut violations = Vec::new();
676 for policy in &self.0 {
678 let directive = policy
680 .directive_set
681 .iter()
682 .find(|directive| directive.name == "script-src")
685 .or_else(|| {
688 policy
689 .directive_set
690 .iter()
691 .find(|directive| directive.name == "default-src")
692 });
693 let Some(directive) = directive else { continue };
694 let source_list = SourceList(&directive.value);
695 if source_list.does_a_source_list_allow_wasm_evaluation() == AllowResult::Allows {
700 continue;
701 }
702 violations.push(Violation {
705 resource: ViolationResource::WasmEval,
707 directive: directive.clone(),
708 policy: policy.clone(),
709 });
710 if policy.disposition == PolicyDisposition::Enforce {
712 result = CheckResult::Blocked
713 }
714 }
715 (result, violations)
716 }
717 pub fn should_navigation_request_be_blocked<TrustedTypesUrlProcessor>(
728 &self,
729 request: &mut Request,
730 navigation_check_type: NavigationCheckType,
731 mut url_processor: TrustedTypesUrlProcessor,
732 ) -> (CheckResult, Vec<Violation>)
733 where
734 TrustedTypesUrlProcessor: FnMut(&str) -> Option<String>,
735 {
736 let mut result = CheckResult::Allowed;
738 let mut violations = Vec::new();
739 for policy in &self.0 {
741 for directive in &policy.directive_set {
743 if directive.pre_navigation_check(
746 request,
747 navigation_check_type,
748 &mut url_processor,
749 policy,
750 ) == CheckResult::Allowed
751 {
752 continue;
753 }
754 violations.push(Violation {
758 resource: ViolationResource::Url(request.url.clone()),
760 directive: Directive {
761 name: get_the_effective_directive_for_request(request).to_owned(),
762 value: directive.value.clone(),
763 },
764 policy: policy.clone(),
765 });
766 if policy.disposition == PolicyDisposition::Enforce {
768 result = CheckResult::Blocked;
769 }
770 }
771 }
772 if result == CheckResult::Allowed && request.current_url.scheme() == "javascript" {
774 for policy in &self.0 {
776 for directive in &policy.directive_set {
778 if directive.inline_check(
781 &Element { nonce: None },
782 InlineCheckType::Navigation,
783 policy,
784 request.current_url.as_str(),
785 ) == CheckResult::Allowed
786 {
787 continue;
788 }
789 violations.push(Violation {
793 resource: ViolationResource::Inline { sample: None },
795 directive: Directive {
796 name: get_the_effective_directive_for_inline_checks(
799 InlineCheckType::Navigation,
800 )
801 .to_owned(),
802 value: directive.value.clone(),
803 },
804 policy: policy.clone(),
805 });
806 if policy.disposition == PolicyDisposition::Enforce {
808 result = CheckResult::Blocked;
809 }
810 }
811 }
812 }
813 (result, violations)
814 }
815 pub fn should_navigation_response_to_navigation_request_be_blocked(
817 &self,
818 response: &Response,
819 self_origin: &Origin,
820 parent_navigable_origins: &Vec<Url>,
821 ) -> (CheckResult, Vec<Violation>) {
822 let mut result = CheckResult::Allowed;
824 let mut violations = Vec::new();
825 for policy in &self.0 {
827 for directive in &policy.directive_set {
829 if directive.navigation_response_check(
833 response,
834 self_origin,
835 parent_navigable_origins,
836 policy,
837 ) == CheckResult::Allowed
838 {
839 continue;
840 }
841 violations.push(Violation {
844 resource: ViolationResource::Url(response.url.clone()),
846 directive: directive.clone(),
847 policy: policy.clone(),
848 });
849 if policy.disposition == PolicyDisposition::Enforce {
851 result = CheckResult::Blocked;
852 }
853 }
854 }
855 (result, violations)
859 }
860}
861
862#[derive(Clone, Debug)]
863pub struct Element<'a> {
864 pub nonce: Option<Cow<'a, str>>,
870}
871
872#[derive(Clone, Copy, Debug, Eq, PartialEq)]
878pub enum InlineCheckType {
879 Script,
880 ScriptAttribute,
881 Style,
882 StyleAttribute,
883 Navigation,
884}
885
886#[derive(Clone, Copy, Debug, Eq, PartialEq)]
892pub enum NavigationCheckType {
893 FormSubmission,
894 Other,
895}
896
897#[derive(Clone, Debug)]
903pub struct Request {
904 pub url: Url,
905 pub current_url: Url,
906 pub origin: Origin,
907 pub redirect_count: u32,
908 pub destination: Destination,
909 pub initiator: Initiator,
910 pub nonce: String,
911 pub integrity_metadata: String,
912 pub parser_metadata: ParserMetadata,
913}
914
915#[derive(Clone, Copy, Debug, Eq, PartialEq)]
916pub enum ParserMetadata {
917 ParserInserted,
918 NotParserInserted,
919 None,
920}
921
922#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
923#[derive(Clone, Copy, Debug, Eq, PartialEq)]
924pub enum Initiator {
925 Download,
926 ImageSet,
927 Manifest,
928 Prefetch,
929 Prerender,
930 Fetch,
931 Xslt,
932 None,
933}
934
935#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
936#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
937pub enum Destination {
938 None,
939 Audio,
940 AudioWorklet,
941 Document,
942 Embed,
943 Font,
944 Frame,
945 IFrame,
946 Image,
947 Json,
948 Manifest,
949 Object,
950 PaintWorklet,
951 Report,
952 Script,
953 ServiceWorker,
954 SharedWorker,
955 Style,
956 Track,
957 Video,
958 WebIdentity,
959 Worker,
960 Xslt,
961}
962
963pub struct InvalidDestination;
964
965impl FromStr for Destination {
966 type Err = InvalidDestination;
967
968 fn from_str(s: &str) -> Result<Self, Self::Err> {
969 let destination = match s {
970 "" => Self::None,
971 "audio" => Self::Audio,
972 "audioworklet" => Self::AudioWorklet,
973 "document" => Self::Document,
974 "embed" => Self::Embed,
975 "font" => Self::Font,
976 "frame" => Self::Frame,
977 "iframe" => Self::IFrame,
978 "image" => Self::Image,
979 "json" => Self::Json,
980 "manifest" => Self::Manifest,
981 "object" => Self::Object,
982 "paintworklet" => Self::PaintWorklet,
983 "report" => Self::Report,
984 "script" => Self::Script,
985 "serviceworker" => Self::ServiceWorker,
986 "sharedworker" => Self::SharedWorker,
987 "style" => Self::Style,
988 "track" => Self::Track,
989 "video" => Self::Video,
990 "webidentity" => Self::WebIdentity,
991 "worker" => Self::Worker,
992 "xslt" => Self::Xslt,
993 _ => return Err(InvalidDestination),
994 };
995
996 Ok(destination)
997 }
998}
999
1000impl Destination {
1001 pub fn is_script_like(self) -> bool {
1003 use Destination::*;
1004 matches!(
1005 self,
1006 AudioWorklet | PaintWorklet | Script | ServiceWorker | SharedWorker | Worker | Xslt
1007 )
1008 }
1009
1010 pub const fn as_str(&self) -> &'static str {
1011 match self {
1012 Self::None => "",
1013 Self::Audio => "audio",
1014 Self::AudioWorklet => "audioworklet",
1015 Self::Document => "document",
1016 Self::Embed => "embed",
1017 Self::Font => "font",
1018 Self::Frame => "frame",
1019 Self::IFrame => "iframe",
1020 Self::Image => "image",
1021 Self::Json => "json",
1022 Self::Manifest => "manifest",
1023 Self::Object => "object",
1024 Self::PaintWorklet => "paintworklet",
1025 Self::Report => "report",
1026 Self::Script => "script",
1027 Self::ServiceWorker => "serviceworker",
1028 Self::SharedWorker => "sharedworker",
1029 Self::Style => "style",
1030 Self::Track => "track",
1031 Self::Video => "video",
1032 Self::WebIdentity => "webidentity",
1033 Self::Worker => "worker",
1034 Self::Xslt => "xslt",
1035 }
1036 }
1037}
1038
1039#[derive(Clone, Debug)]
1044pub struct Response {
1045 pub url: Url,
1046 pub redirect_count: u32,
1047}
1048
1049fn is_local_url(url: &Url) -> bool {
1051 let scheme = url.scheme();
1053 scheme == "about" || scheme == "blob" || scheme == "data"
1055}
1056
1057#[derive(Clone, Debug)]
1063#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1064pub struct Violation {
1065 pub resource: ViolationResource,
1066 pub directive: Directive,
1067 pub policy: Policy,
1068}
1069
1070#[derive(Clone, Debug, Eq, PartialEq)]
1076#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1077pub enum ViolationResource {
1078 Url(Url),
1079 Inline { sample: Option<String> },
1080 TrustedTypePolicy { sample: String },
1081 TrustedTypeSink { sample: String },
1082 Eval { sample: Option<String> },
1083 WasmEval,
1084}
1085
1086#[derive(Clone, Debug, Eq, PartialEq)]
1091#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1092pub enum CheckResult {
1093 Allowed,
1094 Blocked,
1095}
1096
1097#[derive(Clone, Debug, Eq, PartialEq)]
1101#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1102pub enum Violates {
1103 DoesNotViolate,
1104 Directive(Directive),
1105}
1106
1107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1109#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1110pub enum PolicyDisposition {
1111 Enforce,
1112 Report,
1113}
1114
1115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1117#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1118pub enum PolicySource {
1119 Header,
1120 Meta,
1121}
1122
1123#[derive(Clone, Debug, Eq, PartialEq)]
1125#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1126pub struct Directive {
1127 pub name: String,
1128 pub value: Vec<String>,
1129}
1130
1131impl Display for Directive {
1132 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
1133 <str as Display>::fmt(&self.name[..], f)?;
1134 write!(f, " ")?;
1135 for (i, token) in self.value.iter().enumerate() {
1136 if i != 0 {
1137 write!(f, " ")?;
1138 }
1139 <str as Display>::fmt(&token[..], f)?;
1140 }
1141 Ok(())
1142 }
1143}
1144
1145impl Directive {
1146 pub fn is_valid(&self) -> bool {
1148 DIRECTIVE_NAME_GRAMMAR.is_match(&self.name)
1149 && self
1150 .value
1151 .iter()
1152 .all(|t| DIRECTIVE_VALUE_TOKEN_GRAMMAR.is_match(&t[..]))
1153 }
1154 pub fn pre_request_check(&self, request: &Request, policy: &Policy) -> CheckResult {
1156 use CheckResult::*;
1157 match &self.name[..] {
1158 "child-src" => {
1159 let name = get_the_effective_directive_for_request(request);
1160 if !should_fetch_directive_execute(name, "child-src", policy) {
1161 return Allowed;
1162 }
1163 (Directive {
1164 name: String::from(name),
1165 value: self.value.clone(),
1166 })
1167 .pre_request_check(request, policy)
1168 }
1169 "connect-src" => {
1170 let name = get_the_effective_directive_for_request(request);
1171 if !should_fetch_directive_execute(name, "connect-src", policy) {
1172 return Allowed;
1173 }
1174 if SourceList(&self.value[..]).does_request_match_source_list(request)
1175 == DoesNotMatch
1176 {
1177 return Blocked;
1178 }
1179 Allowed
1180 }
1181 "default-src" => {
1182 let name = get_the_effective_directive_for_request(request);
1183 if !should_fetch_directive_execute(name, "default-src", policy) {
1184 return Allowed;
1185 }
1186 (Directive {
1187 name: String::from(name),
1188 value: self.value.clone(),
1189 })
1190 .pre_request_check(request, policy)
1191 }
1192 "font-src" => {
1193 let name = get_the_effective_directive_for_request(request);
1194 if !should_fetch_directive_execute(name, "font-src", policy) {
1195 return Allowed;
1196 }
1197 if SourceList(&self.value[..]).does_request_match_source_list(request)
1198 == DoesNotMatch
1199 {
1200 return Blocked;
1201 }
1202 Allowed
1203 }
1204 "frame-src" => {
1205 let name = get_the_effective_directive_for_request(request);
1206 if !should_fetch_directive_execute(name, "frame-src", policy) {
1207 return Allowed;
1208 }
1209 if SourceList(&self.value[..]).does_request_match_source_list(request)
1210 == DoesNotMatch
1211 {
1212 return Blocked;
1213 }
1214 Allowed
1215 }
1216 "img-src" => {
1217 let name = get_the_effective_directive_for_request(request);
1218 if !should_fetch_directive_execute(name, "img-src", policy) {
1219 return Allowed;
1220 }
1221 if SourceList(&self.value[..]).does_request_match_source_list(request)
1222 == DoesNotMatch
1223 {
1224 return Blocked;
1225 }
1226 Allowed
1227 }
1228 "manifest-src" => {
1229 let name = get_the_effective_directive_for_request(request);
1230 if !should_fetch_directive_execute(name, "manifest-src", policy) {
1231 return Allowed;
1232 }
1233 if SourceList(&self.value[..]).does_request_match_source_list(request)
1234 == DoesNotMatch
1235 {
1236 return Blocked;
1237 }
1238 Allowed
1239 }
1240 "media-src" => {
1241 let name = get_the_effective_directive_for_request(request);
1242 if !should_fetch_directive_execute(name, "media-src", policy) {
1243 return Allowed;
1244 }
1245 if SourceList(&self.value[..]).does_request_match_source_list(request)
1246 == DoesNotMatch
1247 {
1248 return Blocked;
1249 }
1250 Allowed
1251 }
1252 "object-src" => {
1253 let name = get_the_effective_directive_for_request(request);
1254 if !should_fetch_directive_execute(name, "object-src", policy) {
1255 return Allowed;
1256 }
1257 if SourceList(&self.value[..]).does_request_match_source_list(request)
1258 == DoesNotMatch
1259 {
1260 return Blocked;
1261 }
1262 Allowed
1263 }
1264 "script-src" => {
1265 let name = get_the_effective_directive_for_request(request);
1266 if !should_fetch_directive_execute(name, "script-src", policy) {
1267 return Allowed;
1268 }
1269 script_directives_prerequest_check(request, self)
1270 }
1271 "script-src-elem" => {
1272 let name = get_the_effective_directive_for_request(request);
1273 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1274 return Allowed;
1275 }
1276 script_directives_prerequest_check(request, self)
1277 }
1278 "style-src" => {
1279 let name = get_the_effective_directive_for_request(request);
1280 if !should_fetch_directive_execute(name, "style-src", policy) {
1281 return Allowed;
1282 }
1283 let source_list = SourceList(&self.value);
1284 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1285 return Allowed;
1286 }
1287 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1288 return Blocked;
1289 }
1290 Allowed
1291 }
1292 "style-src-elem" => {
1293 let name = get_the_effective_directive_for_request(request);
1294 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1295 return Allowed;
1296 }
1297 let source_list = SourceList(&self.value);
1298 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1299 return Allowed;
1300 }
1301 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1302 return Blocked;
1303 }
1304 Allowed
1305 }
1306 "worker-src" => {
1307 let name = get_the_effective_directive_for_request(request);
1308 if !should_fetch_directive_execute(name, "worker-src", policy) {
1309 return Allowed;
1310 }
1311 let source_list = SourceList(&self.value);
1312 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1313 return Blocked;
1314 }
1315 Allowed
1316 }
1317 _ => Allowed,
1318 }
1319 }
1320 pub fn post_request_check(
1322 &self,
1323 request: &Request,
1324 response: &Response,
1325 policy: &Policy,
1326 ) -> CheckResult {
1327 use CheckResult::*;
1328 match &self.name[..] {
1329 "child-src" => {
1330 let name = get_the_effective_directive_for_request(request);
1331 if !should_fetch_directive_execute(name, "child-src", policy) {
1332 return Allowed;
1333 }
1334 Directive {
1335 name: name.to_owned(),
1336 value: self.value.clone(),
1337 }
1338 .post_request_check(request, response, policy)
1339 }
1340 "connect-src" => {
1341 let name = get_the_effective_directive_for_request(request);
1342 if !should_fetch_directive_execute(name, "connect-src", policy) {
1343 return Allowed;
1344 }
1345 let source_list = SourceList(&self.value);
1346 if source_list.does_response_to_request_match_source_list(request, response)
1347 == DoesNotMatch
1348 {
1349 return Blocked;
1350 }
1351 Allowed
1352 }
1353 "default-src" => {
1354 let name = get_the_effective_directive_for_request(request);
1355 if !should_fetch_directive_execute(name, "default-src", policy) {
1356 return Allowed;
1357 }
1358 Directive {
1359 name: name.to_owned(),
1360 value: self.value.clone(),
1361 }
1362 .post_request_check(request, response, policy)
1363 }
1364 "font-src" => {
1365 let name = get_the_effective_directive_for_request(request);
1366 if !should_fetch_directive_execute(name, "font-src", policy) {
1367 return Allowed;
1368 }
1369 let source_list = SourceList(&self.value);
1370 if source_list.does_response_to_request_match_source_list(request, response)
1371 == DoesNotMatch
1372 {
1373 return Blocked;
1374 }
1375 Allowed
1376 }
1377 "frame-src" => {
1378 let name = get_the_effective_directive_for_request(request);
1379 if !should_fetch_directive_execute(name, "frame-src", policy) {
1380 return Allowed;
1381 }
1382 let source_list = SourceList(&self.value);
1383 if source_list.does_response_to_request_match_source_list(request, response)
1384 == DoesNotMatch
1385 {
1386 return Blocked;
1387 }
1388 Allowed
1389 }
1390 "img-src" => {
1391 let name = get_the_effective_directive_for_request(request);
1392 if !should_fetch_directive_execute(name, "img-src", policy) {
1393 return Allowed;
1394 }
1395 let source_list = SourceList(&self.value);
1396 if source_list.does_response_to_request_match_source_list(request, response)
1397 == DoesNotMatch
1398 {
1399 return Blocked;
1400 }
1401 Allowed
1402 }
1403 "manifest-src" => {
1404 let name = get_the_effective_directive_for_request(request);
1405 if !should_fetch_directive_execute(name, "manifest-src", policy) {
1406 return Allowed;
1407 }
1408 let source_list = SourceList(&self.value);
1409 if source_list.does_response_to_request_match_source_list(request, response)
1410 == DoesNotMatch
1411 {
1412 return Blocked;
1413 }
1414 Allowed
1415 }
1416 "media-src" => {
1417 let name = get_the_effective_directive_for_request(request);
1418 if !should_fetch_directive_execute(name, "media-src", policy) {
1419 return Allowed;
1420 }
1421 let source_list = SourceList(&self.value);
1422 if source_list.does_response_to_request_match_source_list(request, response)
1423 == DoesNotMatch
1424 {
1425 return Blocked;
1426 }
1427 Allowed
1428 }
1429 "object-src" => {
1430 let name = get_the_effective_directive_for_request(request);
1431 if !should_fetch_directive_execute(name, "object-src", policy) {
1432 return Allowed;
1433 }
1434 let source_list = SourceList(&self.value);
1435 if source_list.does_response_to_request_match_source_list(request, response)
1436 == DoesNotMatch
1437 {
1438 return Blocked;
1439 }
1440 Allowed
1441 }
1442 "script-src" => {
1443 let name = get_the_effective_directive_for_request(request);
1444 if !should_fetch_directive_execute(name, "script-src", policy) {
1445 return Allowed;
1446 }
1447 script_directives_postrequest_check(request, response, self)
1448 }
1449 "script-src-elem" => {
1450 let name = get_the_effective_directive_for_request(request);
1451 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1452 return Allowed;
1453 }
1454 script_directives_postrequest_check(request, response, self)
1455 }
1456 "style-src" => {
1457 let name = get_the_effective_directive_for_request(request);
1458 if !should_fetch_directive_execute(name, "style-src", policy) {
1459 return Allowed;
1460 }
1461 let source_list = SourceList(&self.value);
1462 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1463 return Allowed;
1464 }
1465 if source_list.does_response_to_request_match_source_list(request, response)
1466 == DoesNotMatch
1467 {
1468 return Blocked;
1469 }
1470 Allowed
1471 }
1472 "style-src-elem" => {
1473 let name = get_the_effective_directive_for_request(request);
1474 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1475 return Allowed;
1476 }
1477 let source_list = SourceList(&self.value);
1478 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1479 return Allowed;
1480 }
1481 if source_list.does_response_to_request_match_source_list(request, response)
1482 == DoesNotMatch
1483 {
1484 return Blocked;
1485 }
1486 Allowed
1487 }
1488 "worker-src" => {
1489 let name = get_the_effective_directive_for_request(request);
1490 if !should_fetch_directive_execute(name, "worker-src", policy) {
1491 return Allowed;
1492 }
1493 let source_list = SourceList(&self.value);
1494 if source_list.does_response_to_request_match_source_list(request, response)
1495 == DoesNotMatch
1496 {
1497 return Blocked;
1498 }
1499 Allowed
1500 }
1501 _ => Allowed,
1502 }
1503 }
1504 pub fn inline_check(
1506 &self,
1507 element: &Element,
1508 type_: InlineCheckType,
1509 policy: &Policy,
1510 source: &str,
1511 ) -> CheckResult {
1512 use CheckResult::*;
1513 match &self.name[..] {
1514 "default-src" => {
1515 let name = get_the_effective_directive_for_inline_checks(type_);
1516 if !should_fetch_directive_execute(name, "default-src", policy) {
1517 return Allowed;
1518 }
1519 Directive {
1520 name: name.to_owned(),
1521 value: self.value.clone(),
1522 }
1523 .inline_check(element, type_, policy, source)
1524 }
1525 "script-src" => {
1526 let name = get_the_effective_directive_for_inline_checks(type_);
1527 if !should_fetch_directive_execute(name, "script-src", policy) {
1528 return Allowed;
1529 }
1530 let source_list = SourceList(&self.value);
1531 if source_list
1532 .does_element_match_source_list_for_type_and_source(element, type_, source)
1533 == DoesNotMatch
1534 {
1535 return Blocked;
1536 }
1537 Allowed
1538 }
1539 "script-src-elem" => {
1540 let name = get_the_effective_directive_for_inline_checks(type_);
1541 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1542 return Allowed;
1543 }
1544 let source_list = SourceList(&self.value);
1545 if source_list
1546 .does_element_match_source_list_for_type_and_source(element, type_, source)
1547 == DoesNotMatch
1548 {
1549 return Blocked;
1550 }
1551 Allowed
1552 }
1553 "script-src-attr" => {
1554 let name = get_the_effective_directive_for_inline_checks(type_);
1555 if !should_fetch_directive_execute(name, "script-src-attr", policy) {
1556 return Allowed;
1557 }
1558 let source_list = SourceList(&self.value);
1559 if source_list
1560 .does_element_match_source_list_for_type_and_source(element, type_, source)
1561 == DoesNotMatch
1562 {
1563 return Blocked;
1564 }
1565 Allowed
1566 }
1567 "style-src" => {
1568 let name = get_the_effective_directive_for_inline_checks(type_);
1569 if !should_fetch_directive_execute(name, "style-src", policy) {
1570 return Allowed;
1571 }
1572 let source_list = SourceList(&self.value);
1573 if source_list
1574 .does_element_match_source_list_for_type_and_source(element, type_, source)
1575 == DoesNotMatch
1576 {
1577 return Blocked;
1578 }
1579 Allowed
1580 }
1581 "style-src-elem" => {
1582 let name = get_the_effective_directive_for_inline_checks(type_);
1583 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1584 return Allowed;
1585 }
1586 let source_list = SourceList(&self.value);
1587 if source_list
1588 .does_element_match_source_list_for_type_and_source(element, type_, source)
1589 == DoesNotMatch
1590 {
1591 return Blocked;
1592 }
1593 Allowed
1594 }
1595 "style-src-attr" => {
1596 let name = get_the_effective_directive_for_inline_checks(type_);
1597 if !should_fetch_directive_execute(name, "style-src-attr", policy) {
1598 return Allowed;
1599 }
1600 let source_list = SourceList(&self.value);
1601 if source_list
1602 .does_element_match_source_list_for_type_and_source(element, type_, source)
1603 == DoesNotMatch
1604 {
1605 return Blocked;
1606 }
1607 Allowed
1608 }
1609 _ => Allowed,
1610 }
1611 }
1612 pub fn get_sandboxing_flag_set_for_document(
1614 &self,
1615 policy: &Policy,
1616 ) -> Option<SandboxingFlagSet> {
1617 debug_assert!(&self.name[..] == "sandbox");
1618 if policy.disposition != PolicyDisposition::Enforce {
1620 None
1621 } else {
1622 Some(parse_a_sandboxing_directive(&self.value[..]))
1624 }
1625 }
1626 pub fn pre_navigation_check<TrustedTypesUrlProcessor>(
1628 &self,
1629 request: &mut Request,
1630 type_: NavigationCheckType,
1631 mut url_processor: TrustedTypesUrlProcessor,
1632 _policy: &Policy,
1633 ) -> CheckResult
1634 where
1635 TrustedTypesUrlProcessor: FnMut(&str) -> Option<String>,
1636 {
1637 use CheckResult::*;
1638 match &self.name[..] {
1639 "form-action" => {
1641 if type_ == NavigationCheckType::FormSubmission {
1643 let source_list = SourceList(&self.value);
1644 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1647 return Blocked;
1648 }
1649 }
1650 Allowed
1652 }
1653 "require-trusted-types-for" => {
1655 let url = &request.url;
1656 if url.scheme() != "javascript" {
1658 return Allowed;
1659 }
1660 let encoded_script_source = &url[Position::AfterScheme..][1..];
1665 let Some(converted_script_source) = url_processor(encoded_script_source) else {
1669 return Blocked;
1670 };
1671 let url_string = "javascript:".to_owned() + &converted_script_source;
1673 let Ok(new_url) = Url::parse(&url_string) else {
1676 return Blocked;
1677 };
1678 request.url = new_url;
1680 Allowed
1682 }
1683 _ => Allowed,
1684 }
1685 }
1686
1687 pub fn navigation_response_check(
1688 &self,
1689 response: &Response,
1690 self_origin: &Origin,
1691 parent_navigable_origins: &Vec<Url>,
1692 _policy: &Policy,
1693 ) -> CheckResult {
1694 use CheckResult::*;
1695 match &self.name[..] {
1696 "frame-ancestors" => {
1698 if is_local_url(&response.url) {
1700 return Allowed;
1701 }
1702 let source_list = SourceList(&self.value);
1711 for origin in parent_navigable_origins {
1715 if source_list.does_url_match_source_list_in_origin_with_redirect_count(
1720 origin,
1721 self_origin,
1722 0,
1723 ) == DoesNotMatch
1724 {
1725 return Blocked;
1726 }
1727 }
1729 Allowed
1731 }
1732 _ => Allowed,
1733 }
1734 }
1735}
1736
1737fn get_the_effective_directive_for_inline_checks(type_: InlineCheckType) -> &'static str {
1739 use InlineCheckType::*;
1740 match type_ {
1741 Script | Navigation => "script-src-elem",
1742 ScriptAttribute => "script-src-attr",
1743 Style => "style-src-elem",
1744 StyleAttribute => "style-src-attr",
1745 }
1746}
1747
1748fn script_directives_prerequest_check(request: &Request, directive: &Directive) -> CheckResult {
1750 use CheckResult::*;
1751 if request_is_script_like(request) {
1753 let source_list = SourceList(&directive.value[..]);
1754 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1757 return Allowed;
1758 }
1759 if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata)
1762 == Matches
1763 {
1764 return Allowed;
1765 }
1766 if directive
1769 .value
1770 .iter()
1771 .any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'"))
1772 {
1773 if request.parser_metadata == ParserMetadata::ParserInserted {
1775 return Blocked;
1776 }
1777 return Allowed;
1779 }
1780
1781 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1784 return Blocked;
1785 }
1786 }
1787 Allowed
1789}
1790
1791fn script_directives_postrequest_check(
1793 request: &Request,
1794 response: &Response,
1795 directive: &Directive,
1796) -> CheckResult {
1797 use CheckResult::*;
1798 if request_is_script_like(request) {
1800 let source_list = SourceList(&directive.value[..]);
1803 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1806 return Allowed;
1807 }
1808 if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata)
1811 == Matches
1812 {
1813 return Allowed;
1814 }
1815 if directive
1817 .value
1818 .iter()
1819 .any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'"))
1820 {
1821 if request.parser_metadata == ParserMetadata::ParserInserted {
1823 return Blocked;
1824 }
1825 return Allowed;
1827 }
1828 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch
1831 {
1832 return Blocked;
1833 }
1834 }
1835 Allowed
1837}
1838
1839fn request_is_script_like(request: &Request) -> bool {
1841 request.destination.is_script_like()
1842}
1843
1844fn should_fetch_directive_execute(
1846 effective_directive_name: &str,
1847 directive_name: &str,
1848 policy: &Policy,
1849) -> bool {
1850 let directive_fallback_list = get_fetch_directive_fallback_list(effective_directive_name);
1851 for fallback_directive in directive_fallback_list {
1852 if directive_name == *fallback_directive {
1853 return true;
1854 }
1855 if policy.contains_a_directive_whose_name_is(fallback_directive) {
1856 return false;
1857 }
1858 }
1859 false
1860}
1861
1862fn get_fetch_directive_fallback_list(directive_name: &str) -> &'static [&'static str] {
1864 match directive_name {
1865 "script-src-elem" => &["script-src-elem", "script-src", "default-src"],
1866 "script-src-attr" => &["script-src-attr", "script-src", "default-src"],
1867 "style-src-elem" => &["style-src-elem", "style-src", "default-src"],
1868 "style-src-attr" => &["style-src-attr", "style-src", "default-src"],
1869 "worker-src" => &["worker-src", "child-src", "script-src", "default-src"],
1870 "connect-src" => &["connect-src", "default-src"],
1871 "manifest-src" => &["manifest-src", "default-src"],
1872 "object-src" => &["object-src", "default-src"],
1873 "frame-src" => &["frame-src", "child-src", "default-src"],
1874 "media-src" => &["media-src", "default-src"],
1875 "font-src" => &["font-src", "default-src"],
1876 "img-src" => &["img-src", "default-src"],
1877 _ => &[],
1878 }
1879}
1880
1881fn get_the_effective_directive_for_request(request: &Request) -> &'static str {
1883 use Destination::*;
1884 use Initiator::*;
1885 if request.initiator == Prefetch || request.initiator == Prerender {
1887 return "default-src";
1888 }
1889 match request.destination {
1891 Destination::Manifest => "manifest-src",
1892 Object | Embed => "object-src",
1893 Frame | IFrame => "frame-src",
1894 Audio | Track | Video => "media-src",
1895 Font => "font-src",
1896 Image => "img-src",
1897 Style => "style-src-elem",
1898 Script | Destination::Xslt | AudioWorklet | PaintWorklet => "script-src-elem",
1899 ServiceWorker | SharedWorker | Worker => "worker-src",
1900 Json | WebIdentity => "connect-src",
1901 Report => "",
1902 _ => "connect-src",
1904 }
1905}
1906
1907#[derive(Clone, Debug, Eq, PartialEq)]
1909pub enum MatchResult {
1910 Matches,
1911 DoesNotMatch,
1912}
1913
1914static DIRECTIVE_NAME_GRAMMAR: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^[0-9a-z\-]+$"#).unwrap());
1916static DIRECTIVE_VALUE_TOKEN_GRAMMAR: Lazy<Regex> =
1918 Lazy::new(|| Regex::new(r#"^[\u{21}-\u{2B}\u{2D}-\u{3A}\u{3C}-\u{7E}]+$"#).unwrap());
1919static NONCE_SOURCE_GRAMMAR: Lazy<Regex> =
1921 Lazy::new(|| Regex::new(r#"^'nonce-(?P<n>[a-zA-Z0-9\+/\-_]+=*)'$"#).unwrap());
1922static NONE_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^'none'$"#).unwrap());
1923static SCHEME_SOURCE_GRAMMAR: Lazy<Regex> =
1925 Lazy::new(|| Regex::new(r#"^(?P<scheme>[a-zA-Z][a-zA-Z0-9\+\-\.]*):$"#).unwrap());
1926static HOST_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
1928 Regex::new(r#"^((?P<scheme>[a-zA-Z][a-zA-Z0-9\+\-\.]*)://)?(?P<host>\*|(\*\.)?[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*)(?P<port>:(\*|[0-9]+))?(?P<path>/([:@%!\$&'\(\)\*\+,;=0-9a-zA-Z\-\._~]+)?(/[:@%!\$&'\(\)\*\+,;=0-9a-zA-Z\-\._~]*)*)?$"#).unwrap()
1929});
1930static HASH_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
1932 Regex::new(r#"^'(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)'$"#)
1933 .unwrap()
1934});
1935
1936#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1938struct SourceList<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item = &'a U>>(I);
1939
1940impl<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item = &'a U>> SourceList<'a, U, I> {
1941 fn does_nonce_match_source_list(&self, nonce: &str) -> MatchResult {
1943 if nonce.is_empty() {
1944 return DoesNotMatch;
1945 };
1946 for expression in self.0.clone().into_iter() {
1947 if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression.borrow()) {
1948 if let Some(captured_nonce) = captures.name("n") {
1949 if nonce == captured_nonce.as_str() {
1950 return Matches;
1951 }
1952 }
1953 }
1954 }
1955 DoesNotMatch
1956 }
1957 fn does_integrity_metadata_match_source_list(&self, integrity_metadata: &str) -> MatchResult {
1959 let integrity_expressions: Vec<HashFunction> = self
1961 .0
1962 .clone()
1963 .into_iter()
1964 .filter_map(|expression| {
1965 if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression.borrow()) {
1966 if let (Some(algorithm), Some(value)) = (
1967 captures
1968 .name("algorithm")
1969 .and_then(|a| HashAlgorithm::from_name(a.as_str())),
1970 captures.name("value"),
1971 ) {
1972 return Some(HashFunction {
1973 algorithm,
1974 value: String::from(value.as_str()),
1975 });
1976 }
1977 }
1978 None
1979 })
1980 .collect();
1981 if integrity_expressions.is_empty() {
1983 return DoesNotMatch;
1984 }
1985 let integrity_sources = parse_subresource_integrity_metadata(integrity_metadata);
1987 match integrity_sources {
1988 SubresourceIntegrityMetadata::NoMetadata => DoesNotMatch,
1990 SubresourceIntegrityMetadata::IntegritySources(integrity_sources) => {
1991 if integrity_sources.is_empty() {
1992 return DoesNotMatch;
1993 }
1994 for source in &integrity_sources {
1996 if !integrity_expressions.iter().any(|ex| ex == source) {
2003 return DoesNotMatch;
2004 }
2005 }
2006 Matches
2008 }
2009 }
2010 }
2011 fn does_request_match_source_list(&self, request: &Request) -> MatchResult {
2013 self.does_url_match_source_list_in_origin_with_redirect_count(
2018 &request.current_url,
2019 &request.origin,
2020 request.redirect_count,
2021 )
2022 }
2023 fn does_url_match_source_list_in_origin_with_redirect_count(
2025 &self,
2026 url: &Url,
2027 origin: &Origin,
2028 redirect_count: u32,
2029 ) -> MatchResult {
2030 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2031 if NONE_SOURCE_GRAMMAR.is_match(expression) {
2032 continue;
2033 };
2034 let result = does_url_match_expression_in_origin_with_redirect_count(
2035 url,
2036 expression,
2037 origin,
2038 redirect_count,
2039 );
2040 if result == Matches {
2041 return Matches;
2042 }
2043 }
2044 DoesNotMatch
2045 }
2046 fn does_element_match_source_list_for_type_and_source(
2048 &self,
2049 element: &Element,
2050 type_: InlineCheckType,
2051 source: &str,
2052 ) -> MatchResult {
2053 if self.does_a_source_list_allow_all_inline_behavior_for_type(type_) == AllowResult::Allows
2054 {
2055 return Matches;
2056 }
2057 if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style {
2058 if let Some(nonce) = element.nonce.as_ref() {
2059 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2060 if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression) {
2061 if let Some(captured_nonce) = captures.name("n") {
2062 if nonce == captured_nonce.as_str() {
2063 return Matches;
2064 }
2065 }
2066 }
2067 }
2068 }
2069 }
2070 let mut unsafe_hashes = false;
2071 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2072 if ascii_case_insensitive_match(expression, "'unsafe-hashes'") {
2073 unsafe_hashes = true;
2074 break;
2075 }
2076 }
2077 if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style || unsafe_hashes {
2078 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2079 if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression) {
2080 if let (Some(algorithm), Some(value)) = (
2081 captures
2082 .name("algorithm")
2083 .and_then(|a| HashAlgorithm::from_name(a.as_str())),
2084 captures.name("value"),
2085 ) {
2086 let actual = algorithm.apply(source);
2087 let expected = value.as_str().replace('-', "+").replace('_', "/");
2088 if actual == expected {
2089 return Matches;
2090 }
2091 }
2092 }
2093 }
2094 }
2095 DoesNotMatch
2096 }
2097 fn does_a_source_list_allow_all_inline_behavior_for_type(
2099 &self,
2100 type_: InlineCheckType,
2101 ) -> AllowResult {
2102 use InlineCheckType::*;
2103 let mut allow_all_inline = false;
2104 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2105 if HASH_SOURCE_GRAMMAR.is_match(expression) || NONCE_SOURCE_GRAMMAR.is_match(expression)
2106 {
2107 return AllowResult::DoesNotAllow;
2108 }
2109 if (type_ == Script || type_ == ScriptAttribute || type_ == Navigation)
2110 && expression == "'strict-dynamic'"
2111 {
2112 return AllowResult::DoesNotAllow;
2113 }
2114 if ascii_case_insensitive_match(expression, "'unsafe-inline'") {
2115 allow_all_inline = true;
2116 }
2117 }
2118 if allow_all_inline {
2119 AllowResult::Allows
2120 } else {
2121 AllowResult::DoesNotAllow
2122 }
2123 }
2124 fn does_response_to_request_match_source_list(
2126 &self,
2127 request: &Request,
2128 response: &Response,
2129 ) -> MatchResult {
2130 self.does_url_match_source_list_in_origin_with_redirect_count(
2131 &response.url,
2132 &request.origin,
2133 response.redirect_count,
2134 )
2135 }
2136 fn does_a_source_list_allow_js_evaluation(&self) -> AllowResult {
2138 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2139 if ascii_case_insensitive_match(expression, "'unsafe-eval'") {
2142 return AllowResult::Allows;
2143 }
2144 }
2145 AllowResult::DoesNotAllow
2146 }
2147 fn does_a_source_list_allow_wasm_evaluation(&self) -> AllowResult {
2149 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2150 if ascii_case_insensitive_match(expression, "'unsafe-eval'")
2151 || ascii_case_insensitive_match(expression, "'wasm-unsafe-eval'")
2152 {
2153 return AllowResult::Allows;
2154 }
2155 }
2156 AllowResult::DoesNotAllow
2157 }
2158}
2159
2160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2162enum AllowResult {
2163 Allows,
2164 DoesNotAllow,
2165}
2166
2167fn does_url_match_expression_in_origin_with_redirect_count(
2169 url: &Url,
2170 expression: &str,
2171 origin: &Origin,
2172 redirect_count: u32,
2173) -> MatchResult {
2174 let url_scheme = url.scheme();
2175 if expression == "*" {
2176 if scheme_is_network(url_scheme) {
2177 return Matches;
2178 }
2179 return origin_scheme_part_match(origin, url_scheme);
2180 }
2181 if let Some(captures) = SCHEME_SOURCE_GRAMMAR.captures(expression) {
2182 if let Some(expression_scheme) = captures.name("scheme") {
2183 return scheme_part_match(expression_scheme.as_str(), url_scheme);
2184 }
2185 return DoesNotMatch;
2187 }
2188 if let Some(captures) = HOST_SOURCE_GRAMMAR.captures(expression) {
2189 let expr_has_scheme_part = if let Some(expression_scheme) = captures.name("scheme") {
2190 if scheme_part_match(expression_scheme.as_str(), url_scheme) != Matches {
2191 return DoesNotMatch;
2192 }
2193 true
2194 } else {
2195 false
2196 };
2197 let url_host = if let Some(url_host) = url.host() {
2198 url_host
2199 } else {
2200 return DoesNotMatch;
2201 };
2202 if !expr_has_scheme_part && origin_scheme_part_match(origin, url.scheme()) != Matches {
2203 return DoesNotMatch;
2204 }
2205 if let Some(expression_host) = captures.name("host") {
2206 if host_part_match(expression_host.as_str(), &url_host.to_string()) != Matches {
2207 return DoesNotMatch;
2208 }
2209 } else {
2210 return DoesNotMatch;
2212 }
2213 let port_part = captures.name("port").map(|port| &port.as_str()[1..]);
2215 if port_part_match(port_part, url) != Matches {
2216 return DoesNotMatch;
2217 }
2218 let path_part = captures
2219 .name("path")
2220 .map(|path_part| path_part.as_str())
2221 .unwrap_or("");
2222 if path_part != "/" && redirect_count == 0 {
2223 let path = url.path();
2224 if path_part_match(path_part, path) != Matches {
2225 return DoesNotMatch;
2226 }
2227 }
2228 return Matches;
2229 }
2230 if ascii_case_insensitive_match(expression, "'self'") {
2231 if *origin == url.origin() {
2232 return Matches;
2233 }
2234 if let Origin::Tuple(scheme, host, port) = origin {
2235 let hosts_are_the_same = Some(host) == url.host().map(|p| p.to_owned()).as_ref();
2236 let ports_are_the_same = Some(*port) == url.port();
2237 let origins_port_is_default_for_scheme = Some(*port) == default_port(scheme);
2238 let url_port_is_default_port_for_scheme =
2239 url.port() == default_port(scheme) && default_port(scheme).is_some();
2240 let ports_are_default =
2241 url_port_is_default_port_for_scheme && origins_port_is_default_for_scheme;
2242 if hosts_are_the_same
2243 && (ports_are_the_same || ports_are_default)
2244 && ((url_scheme == "https" || url_scheme == "wss")
2245 || (scheme == "http" && (url_scheme == "http" || url_scheme == "ws")))
2246 {
2247 return Matches;
2248 }
2249 }
2250 }
2251 DoesNotMatch
2252}
2253
2254fn host_part_match(pattern: &str, host: &str) -> MatchResult {
2256 debug_assert!(!host.is_empty());
2257 if host.is_empty() {
2259 return DoesNotMatch;
2260 }
2261 if pattern.as_bytes()[0] == b'*' {
2262 if pattern.len() == 1 {
2264 return Matches;
2265 }
2266 if pattern.as_bytes()[1] == b'.' {
2268 let remaining_pattern = &pattern[1..];
2270 if remaining_pattern.len() > host.len() {
2271 return DoesNotMatch;
2272 }
2273 let remaining_host = &host[(host.len() - remaining_pattern.len())..];
2274 debug_assert_eq!(remaining_host.len(), remaining_pattern.len());
2275 if ascii_case_insensitive_match(remaining_pattern, remaining_host) {
2277 return Matches;
2278 }
2279 return DoesNotMatch;
2281 }
2282 }
2283 if !ascii_case_insensitive_match(pattern, host) {
2285 return DoesNotMatch;
2286 }
2287 static IPV4_ADDRESS_RULE: Lazy<Regex> = Lazy::new(|| {
2288 Regex::new(r#"([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])"#).unwrap()
2289 });
2290 if IPV4_ADDRESS_RULE.is_match(pattern) && pattern != "127.0.0.1" {
2291 return DoesNotMatch;
2292 }
2293 if pattern.as_bytes()[0] == b'[' {
2297 return DoesNotMatch;
2298 }
2299 Matches
2301}
2302
2303fn port_part_match(input: Option<&str>, url: &Url) -> MatchResult {
2305 use std::str::FromStr;
2306 debug_assert!(input.is_none() || input == Some("*") || u16::from_str(input.unwrap()).is_ok());
2308 if input == Some("*") {
2310 return Matches;
2311 }
2312 let normalized_input = if let Some(input) = input {
2314 u16::from_str(&input).ok()
2315 } else {
2316 None
2317 };
2318 if normalized_input == url.port() {
2320 return Matches;
2321 }
2322 if url.port().is_none() {
2324 let default_port = default_port(url.scheme());
2326 if normalized_input == default_port {
2328 return Matches;
2329 }
2330 }
2331 DoesNotMatch
2333}
2334
2335fn path_part_match(path_a: &str, path_b: &str) -> MatchResult {
2337 if path_a.is_empty() {
2338 return Matches;
2339 }
2340 if path_a == "/" && path_b.is_empty() {
2341 return Matches;
2342 }
2343 let exact_match = path_a.as_bytes()[path_a.len() - 1] != b'/';
2344 let (mut path_list_a, path_list_b): (Vec<&str>, Vec<&str>) =
2345 (path_a.split('/').collect(), path_b.split('/').collect());
2346 if path_list_a.len() > path_list_b.len() {
2347 return DoesNotMatch;
2348 }
2349 if exact_match && path_list_a.len() != path_list_b.len() {
2350 return DoesNotMatch;
2351 }
2352 if !exact_match {
2353 debug_assert_eq!(path_list_a[path_list_a.len() - 1], "");
2354 path_list_a.pop();
2355 }
2356 let mut piece_b_iter = path_list_b.iter();
2357 for piece_a in &path_list_a {
2358 let piece_b = piece_b_iter.next().unwrap();
2359 let piece_a: Vec<u8> = percent_encoding::percent_decode(piece_a.as_bytes()).collect();
2360 let piece_b: Vec<u8> = percent_encoding::percent_decode(piece_b.as_bytes()).collect();
2361 if piece_a != piece_b {
2362 return DoesNotMatch;
2363 }
2364 }
2365 Matches
2366}
2367
2368fn default_port(scheme: &str) -> Option<u16> {
2369 Some(match scheme {
2370 "ftp" => 21,
2371 "gopher" => 70,
2372 "http" => 80,
2373 "https" => 443,
2374 "ws" => 80,
2375 "wss" => 443,
2376 _ => return None,
2377 })
2378}
2379
2380fn origin_scheme_part_match(a: &Origin, b: &str) -> MatchResult {
2381 if let Origin::Tuple(scheme, _host, _port) = a {
2382 scheme_part_match(&scheme[..], b)
2383 } else {
2384 DoesNotMatch
2385 }
2386}
2387
2388fn scheme_part_match(a: &str, b: &str) -> MatchResult {
2390 let a = a.to_ascii_lowercase();
2391 let b = b.to_ascii_lowercase();
2392 match (&a[..], &b[..]) {
2393 _ if a == b => Matches,
2394 ("http", "https") | ("ws", "wss" | "http" | "https") | ("wss", "https") => Matches,
2395 _ => DoesNotMatch,
2396 }
2397}
2398
2399#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2400pub enum HashAlgorithm {
2401 Sha256,
2402 Sha384,
2403 Sha512,
2404}
2405
2406impl HashAlgorithm {
2407 pub fn from_name(name: &str) -> Option<HashAlgorithm> {
2408 use HashAlgorithm::*;
2409 match name {
2410 "sha256" | "Sha256" | "sHa256" | "shA256" | "SHa256" | "ShA256" | "sHA256"
2411 | "SHA256" => Some(Sha256),
2412 "sha384" | "Sha384" | "sHa384" | "shA384" | "SHa384" | "ShA384" | "sHA384"
2413 | "SHA384" => Some(Sha384),
2414 "sha512" | "Sha512" | "sHa512" | "shA512" | "SHa512" | "ShA512" | "sHA512"
2415 | "SHA512" => Some(Sha512),
2416 _ => None,
2417 }
2418 }
2419 pub fn apply(self, value: &str) -> String {
2420 use base64::Engine as _;
2421 let bytes = value.as_bytes();
2422 let standard = base64::engine::general_purpose::STANDARD;
2423 match self {
2424 HashAlgorithm::Sha256 => standard.encode(sha2::Sha256::digest(bytes)),
2425 HashAlgorithm::Sha384 => standard.encode(sha2::Sha384::digest(bytes)),
2426 HashAlgorithm::Sha512 => standard.encode(sha2::Sha512::digest(bytes)),
2427 }
2428 }
2429}
2430
2431#[derive(Clone, Debug, Eq, PartialEq)]
2433pub struct HashFunction {
2434 algorithm: HashAlgorithm,
2435 value: String,
2436 }
2438
2439#[derive(Clone, Debug, Eq, PartialEq)]
2441pub enum SubresourceIntegrityMetadata {
2442 NoMetadata,
2443 IntegritySources(Vec<HashFunction>),
2444}
2445
2446static SUBRESOURCE_METADATA_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
2449 Regex::new(r#"(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)"#)
2450 .unwrap()
2451});
2452
2453pub fn parse_subresource_integrity_metadata(string: &str) -> SubresourceIntegrityMetadata {
2455 let mut result = Vec::new();
2456 let mut empty = true;
2457 for token in split_ascii_whitespace(string) {
2458 empty = false;
2459 if let Some(captures) = SUBRESOURCE_METADATA_GRAMMAR.captures(token) {
2460 if let (Some(algorithm), Some(value)) = (
2461 captures
2462 .name("algorithm")
2463 .and_then(|a| HashAlgorithm::from_name(a.as_str())),
2464 captures.name("value"),
2465 ) {
2466 result.push(HashFunction {
2467 algorithm,
2468 value: String::from(value.as_str()),
2469 });
2470 }
2471 }
2472 }
2473 if empty {
2474 SubresourceIntegrityMetadata::NoMetadata
2475 } else {
2476 SubresourceIntegrityMetadata::IntegritySources(result)
2477 }
2478}
2479
2480#[cfg(test)]
2481mod test {
2482 use super::*;
2483 #[test]
2484 fn empty_directive_is_not_valid() {
2485 let d = Directive {
2486 name: String::new(),
2487 value: Vec::new(),
2488 };
2489 assert!(!d.is_valid());
2490 }
2491 #[test]
2492 pub fn duplicate_policy_is_not_valid() {
2493 let d = Directive {
2494 name: "test".to_owned(),
2495 value: vec!["test".to_owned()],
2496 };
2497 let p = Policy {
2498 directive_set: vec![d.clone(), d.clone()],
2499 disposition: PolicyDisposition::Enforce,
2500 source: PolicySource::Header,
2501 };
2502 assert!(!p.is_valid());
2503 }
2504 #[test]
2505 pub fn basic_policy_is_valid() {
2506 let p = Policy::parse(
2507 "script-src notriddle.com",
2508 PolicySource::Header,
2509 PolicyDisposition::Enforce,
2510 );
2511 assert!(p.is_valid());
2512 }
2513 #[test]
2514 pub fn policy_with_empty_directive_set_is_not_valid() {
2515 let p = Policy {
2516 directive_set: vec![],
2517 disposition: PolicyDisposition::Enforce,
2518 source: PolicySource::Header,
2519 };
2520 assert!(!p.is_valid());
2521 }
2522
2523 #[test]
2524 pub fn prefetch_request_does_not_violate_policy() {
2525 let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2526 let request = Request {
2527 url: url.clone(),
2528 current_url: url,
2529 origin: Origin::Tuple(
2530 "https".to_string(),
2531 url::Host::Domain("notriddle.com".to_owned()),
2532 443,
2533 ),
2534 redirect_count: 0,
2535 destination: Destination::Script,
2536 initiator: Initiator::Prefetch,
2537 nonce: String::new(),
2538 integrity_metadata: String::new(),
2539 parser_metadata: ParserMetadata::None,
2540 };
2541
2542 let p = Policy::parse(
2543 "child-src 'self'",
2544 PolicySource::Header,
2545 PolicyDisposition::Enforce,
2546 );
2547
2548 let violation_result = p.does_request_violate_policy(&request);
2549
2550 assert!(violation_result == Violates::DoesNotViolate);
2551 }
2552
2553 #[test]
2554 pub fn prefetch_request_violates_policy() {
2555 let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2556 let request = Request {
2557 url: url.clone(),
2558 current_url: url,
2559 origin: Origin::Tuple(
2560 "https".to_string(),
2561 url::Host::Domain("notriddle.com".to_owned()),
2562 443,
2563 ),
2564 redirect_count: 0,
2565 destination: Destination::ServiceWorker,
2566 initiator: Initiator::None,
2567 nonce: String::new(),
2568 integrity_metadata: String::new(),
2569 parser_metadata: ParserMetadata::None,
2570 };
2571
2572 let p = Policy::parse(
2573 "default-src 'none'; script-src 'self' ",
2574 PolicySource::Header,
2575 PolicyDisposition::Enforce,
2576 );
2577
2578 let violation_result = p.does_request_violate_policy(&request);
2579
2580 let expected_result = Violates::Directive(Directive {
2581 name: String::from("script-src"),
2582 value: vec![String::from("'self'")],
2583 });
2584
2585 assert!(violation_result == expected_result);
2586 }
2587
2588 #[test]
2589 pub fn prefetch_request_is_allowed_by_directive() {
2590 let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2591 let request = Request {
2592 url: url.clone(),
2593 current_url: url,
2594 origin: Origin::Tuple(
2595 "https".to_string(),
2596 url::Host::Domain("notriddle.com".to_owned()),
2597 443,
2598 ),
2599 redirect_count: 0,
2600 destination: Destination::Script,
2601 initiator: Initiator::Prefetch,
2602 nonce: String::new(),
2603 integrity_metadata: String::new(),
2604 parser_metadata: ParserMetadata::None,
2605 };
2606
2607 let p = Policy::parse(
2608 "default-src 'none'; child-src 'self'",
2609 PolicySource::Header,
2610 PolicyDisposition::Enforce,
2611 );
2612
2613 let violation_result = p.does_request_violate_policy(&request);
2614
2615 assert!(violation_result == Violates::DoesNotViolate);
2616 }
2617
2618 #[test]
2619 pub fn websocket_request_is_allowed_by_directive() {
2620 let url = Url::parse("https://www.notriddle.com/websocket").unwrap();
2621 let request = Request {
2622 url: url.clone(),
2623 current_url: url,
2624 origin: Origin::Tuple(
2625 "https".to_string(),
2626 url::Host::Domain("notriddle.com".to_owned()),
2627 443,
2628 ),
2629 redirect_count: 0,
2630 destination: Destination::None,
2631 initiator: Initiator::None,
2632 nonce: String::new(),
2633 integrity_metadata: String::new(),
2634 parser_metadata: ParserMetadata::None,
2635 };
2636
2637 let p = Policy::parse(
2638 "connect-src ws://www.notriddle.com/websocket",
2639 PolicySource::Header,
2640 PolicyDisposition::Enforce,
2641 );
2642
2643 let violation_result = p.does_request_violate_policy(&request);
2644
2645 assert!(violation_result == Violates::DoesNotViolate);
2646 }
2647
2648 #[test]
2649 pub fn trusted_type_policy_is_valid() {
2650 let p = Policy::parse(
2651 "trusted-types 'none'",
2652 PolicySource::Meta,
2653 PolicyDisposition::Enforce,
2654 );
2655 assert!(p.is_valid());
2656 assert_eq!(p.directive_set[0].value, vec!["'none'".to_owned()]);
2657 }
2658
2659 #[test]
2660 pub fn csp_list_is_valid() {
2661 let csp_list = CspList::parse(
2662 "default-src 'none'; child-src 'self', trusted-types 'none'",
2663 PolicySource::Meta,
2664 PolicyDisposition::Enforce,
2665 );
2666 assert!(csp_list.is_valid());
2667 assert_eq!(
2668 csp_list.0[1].directive_set[0].value,
2669 vec!["'none'".to_owned()]
2670 );
2671 }
2672
2673 #[test]
2674 pub fn no_trusted_types_specified_allows_all_policies() {
2675 let csp_list = CspList::parse(
2676 "default-src 'none'; child-src 'self'",
2677 PolicySource::Meta,
2678 PolicyDisposition::Enforce,
2679 );
2680 assert!(csp_list.is_valid());
2681 let (check_result, violations) =
2682 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2683 assert_eq!(check_result, CheckResult::Allowed);
2684 assert!(violations.is_empty());
2685 }
2686
2687 #[test]
2688 pub fn none_does_not_allow_for_any_policy() {
2689 let csp_list = CspList::parse(
2690 "trusted-types 'none'",
2691 PolicySource::Meta,
2692 PolicyDisposition::Enforce,
2693 );
2694 assert!(csp_list.is_valid());
2695 let (check_result, violations) =
2696 csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2697 assert!(check_result == CheckResult::Blocked);
2698 assert_eq!(violations.len(), 1);
2699 }
2700
2701 #[test]
2702 pub fn extra_none_allows_all_policies() {
2703 let csp_list = CspList::parse(
2704 "trusted-types some-policy 'none'",
2705 PolicySource::Meta,
2706 PolicyDisposition::Enforce,
2707 );
2708 assert!(csp_list.is_valid());
2709 let (check_result, violations) =
2710 csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2711 assert!(check_result == CheckResult::Allowed);
2712 assert!(violations.is_empty());
2713 }
2714
2715 #[test]
2716 pub fn explicit_policy_named_is_allowed() {
2717 let csp_list = CspList::parse(
2718 "trusted-types MyPolicy",
2719 PolicySource::Meta,
2720 PolicyDisposition::Enforce,
2721 );
2722 assert!(csp_list.is_valid());
2723 let (check_result, violations) =
2724 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2725 assert_eq!(check_result, CheckResult::Allowed);
2726 assert!(violations.is_empty());
2727 }
2728
2729 #[test]
2730 pub fn other_policy_name_is_blocked() {
2731 let csp_list = CspList::parse(
2732 "trusted-types MyPolicy",
2733 PolicySource::Meta,
2734 PolicyDisposition::Enforce,
2735 );
2736 assert!(csp_list.is_valid());
2737 let (check_result, violations) =
2738 csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2739 assert!(check_result == CheckResult::Blocked);
2740 assert_eq!(violations.len(), 1);
2741 }
2742
2743 #[test]
2744 pub fn invalid_characters_in_policy_name_is_blocked() {
2745 let csp_list = CspList::parse(
2746 "trusted-types My?Policy",
2747 PolicySource::Meta,
2748 PolicyDisposition::Enforce,
2749 );
2750 assert!(csp_list.is_valid());
2751 let (check_result, violations) =
2752 csp_list.is_trusted_type_policy_creation_allowed("My?Policy", &["My?Policy"]);
2753 assert!(check_result == CheckResult::Blocked);
2754 assert_eq!(violations.len(), 1);
2755 }
2756
2757 #[test]
2758 pub fn already_created_policy_is_blocked() {
2759 let csp_list = CspList::parse(
2760 "trusted-types MyPolicy",
2761 PolicySource::Meta,
2762 PolicyDisposition::Enforce,
2763 );
2764 assert!(csp_list.is_valid());
2765 let (check_result, violations) =
2766 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2767 assert!(check_result == CheckResult::Blocked);
2768 assert_eq!(violations.len(), 1);
2769 }
2770
2771 #[test]
2772 pub fn already_created_policy_is_allowed_with_allow_duplicates() {
2773 let csp_list = CspList::parse(
2774 "trusted-types MyPolicy 'allow-duplicates'",
2775 PolicySource::Meta,
2776 PolicyDisposition::Enforce,
2777 );
2778 assert!(csp_list.is_valid());
2779 let (check_result, violations) =
2780 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2781 assert!(check_result == CheckResult::Allowed);
2782 assert!(violations.is_empty());
2783 }
2784
2785 #[test]
2786 pub fn only_report_policy_issues_for_disposition_report() {
2787 let csp_list = CspList::parse(
2788 "trusted-types MyPolicy",
2789 PolicySource::Meta,
2790 PolicyDisposition::Report,
2791 );
2792 assert!(csp_list.is_valid());
2793 let (check_result, violations) =
2794 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2795 assert!(check_result == CheckResult::Allowed);
2796 assert_eq!(violations.len(), 1);
2797 }
2798
2799 #[test]
2800 pub fn wildcard_allows_all_policies() {
2801 let csp_list = CspList::parse(
2802 "trusted-types *",
2803 PolicySource::Meta,
2804 PolicyDisposition::Report,
2805 );
2806 assert!(csp_list.is_valid());
2807 let (check_result, violations) =
2808 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2809 assert!(check_result == CheckResult::Allowed);
2810 assert!(violations.is_empty());
2811 }
2812
2813 #[test]
2814 pub fn violation_has_correct_directive() {
2815 let csp_list = CspList::parse(
2816 "trusted-types MyPolicy",
2817 PolicySource::Meta,
2818 PolicyDisposition::Enforce,
2819 );
2820 assert!(csp_list.is_valid());
2821 let (check_result, violations) =
2822 csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2823 assert!(check_result == CheckResult::Blocked);
2824 assert_eq!(violations.len(), 1);
2825 assert_eq!(violations[0].directive, csp_list.0[0].directive_set[0]);
2826 }
2827
2828 #[test]
2829 pub fn long_policy_name_is_truncated() {
2830 let csp_list = CspList::parse(
2831 "trusted-types MyPolicy",
2832 PolicySource::Meta,
2833 PolicyDisposition::Enforce,
2834 );
2835 assert!(csp_list.is_valid());
2836 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed(
2837 "SuperLongPolicyNameThatExceeds40Characters",
2838 &[],
2839 );
2840 assert!(check_result == CheckResult::Blocked);
2841 assert_eq!(violations.len(), 1);
2842 assert!(
2843 matches!(&violations[0].resource, ViolationResource::TrustedTypePolicy { sample } if sample == "SuperLongPolicyNameThatExceeds40Characte")
2844 );
2845 }
2846}