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()
1930});
1931static HASH_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
1933 Regex::new(r#"^'(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)'$"#)
1934 .unwrap()
1935});
1936
1937#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1939struct SourceList<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item = &'a U>>(I);
1940
1941impl<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item = &'a U>> SourceList<'a, U, I> {
1942 fn does_nonce_match_source_list(&self, nonce: &str) -> MatchResult {
1944 if nonce.is_empty() {
1945 return DoesNotMatch;
1946 };
1947 for expression in self.0.clone().into_iter() {
1948 if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression.borrow()) {
1949 if let Some(captured_nonce) = captures.name("n") {
1950 if nonce == captured_nonce.as_str() {
1951 return Matches;
1952 }
1953 }
1954 }
1955 }
1956 DoesNotMatch
1957 }
1958 fn does_integrity_metadata_match_source_list(&self, integrity_metadata: &str) -> MatchResult {
1960 let integrity_expressions: Vec<HashFunction> = self
1962 .0
1963 .clone()
1964 .into_iter()
1965 .filter_map(|expression| {
1966 if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression.borrow()) {
1967 if let (Some(algorithm), Some(value)) = (
1968 captures
1969 .name("algorithm")
1970 .and_then(|a| HashAlgorithm::from_name(a.as_str())),
1971 captures.name("value"),
1972 ) {
1973 return Some(HashFunction {
1974 algorithm,
1975 value: String::from(value.as_str()),
1976 });
1977 }
1978 }
1979 None
1980 })
1981 .collect();
1982 if integrity_expressions.is_empty() {
1984 return DoesNotMatch;
1985 }
1986 let integrity_sources = parse_subresource_integrity_metadata(integrity_metadata);
1988 match integrity_sources {
1989 SubresourceIntegrityMetadata::NoMetadata => DoesNotMatch,
1991 SubresourceIntegrityMetadata::IntegritySources(integrity_sources) => {
1992 if integrity_sources.is_empty() {
1993 return DoesNotMatch;
1994 }
1995 for source in &integrity_sources {
1997 if !integrity_expressions.iter().any(|ex| ex == source) {
2004 return DoesNotMatch;
2005 }
2006 }
2007 Matches
2009 }
2010 }
2011 }
2012 fn does_request_match_source_list(&self, request: &Request) -> MatchResult {
2014 self.does_url_match_source_list_in_origin_with_redirect_count(
2019 &request.current_url,
2020 &request.origin,
2021 request.redirect_count,
2022 )
2023 }
2024 fn does_url_match_source_list_in_origin_with_redirect_count(
2026 &self,
2027 url: &Url,
2028 origin: &Origin,
2029 redirect_count: u32,
2030 ) -> MatchResult {
2031 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2032 if NONE_SOURCE_GRAMMAR.is_match(expression) {
2033 continue;
2034 };
2035 let result = does_url_match_expression_in_origin_with_redirect_count(
2036 url,
2037 expression,
2038 origin,
2039 redirect_count,
2040 );
2041 if result == Matches {
2042 return Matches;
2043 }
2044 }
2045 DoesNotMatch
2046 }
2047 fn does_element_match_source_list_for_type_and_source(
2049 &self,
2050 element: &Element,
2051 type_: InlineCheckType,
2052 source: &str,
2053 ) -> MatchResult {
2054 if self.does_a_source_list_allow_all_inline_behavior_for_type(type_) == AllowResult::Allows
2055 {
2056 return Matches;
2057 }
2058 if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style {
2059 if let Some(nonce) = element.nonce.as_ref() {
2060 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2061 if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression) {
2062 if let Some(captured_nonce) = captures.name("n") {
2063 if nonce == captured_nonce.as_str() {
2064 return Matches;
2065 }
2066 }
2067 }
2068 }
2069 }
2070 }
2071 let mut unsafe_hashes = false;
2072 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2073 if ascii_case_insensitive_match(expression, "'unsafe-hashes'") {
2074 unsafe_hashes = true;
2075 break;
2076 }
2077 }
2078 if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style || unsafe_hashes {
2079 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2080 if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression) {
2081 if let (Some(algorithm), Some(value)) = (
2082 captures
2083 .name("algorithm")
2084 .and_then(|a| HashAlgorithm::from_name(a.as_str())),
2085 captures.name("value"),
2086 ) {
2087 let actual = algorithm.apply(source);
2088 let expected = value.as_str().replace('-', "+").replace('_', "/");
2089 if actual == expected {
2090 return Matches;
2091 }
2092 }
2093 }
2094 }
2095 }
2096 DoesNotMatch
2097 }
2098 fn does_a_source_list_allow_all_inline_behavior_for_type(
2100 &self,
2101 type_: InlineCheckType,
2102 ) -> AllowResult {
2103 use InlineCheckType::*;
2104 let mut allow_all_inline = false;
2105 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2106 if HASH_SOURCE_GRAMMAR.is_match(expression) || NONCE_SOURCE_GRAMMAR.is_match(expression)
2107 {
2108 return AllowResult::DoesNotAllow;
2109 }
2110 if (type_ == Script || type_ == ScriptAttribute || type_ == Navigation)
2111 && expression == "'strict-dynamic'"
2112 {
2113 return AllowResult::DoesNotAllow;
2114 }
2115 if ascii_case_insensitive_match(expression, "'unsafe-inline'") {
2116 allow_all_inline = true;
2117 }
2118 }
2119 if allow_all_inline {
2120 AllowResult::Allows
2121 } else {
2122 AllowResult::DoesNotAllow
2123 }
2124 }
2125 fn does_response_to_request_match_source_list(
2127 &self,
2128 request: &Request,
2129 response: &Response,
2130 ) -> MatchResult {
2131 self.does_url_match_source_list_in_origin_with_redirect_count(
2132 &response.url,
2133 &request.origin,
2134 response.redirect_count,
2135 )
2136 }
2137 fn does_a_source_list_allow_js_evaluation(&self) -> AllowResult {
2139 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2140 if ascii_case_insensitive_match(expression, "'unsafe-eval'") {
2143 return AllowResult::Allows;
2144 }
2145 }
2146 AllowResult::DoesNotAllow
2147 }
2148 fn does_a_source_list_allow_wasm_evaluation(&self) -> AllowResult {
2150 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2151 if ascii_case_insensitive_match(expression, "'unsafe-eval'")
2152 || ascii_case_insensitive_match(expression, "'wasm-unsafe-eval'")
2153 {
2154 return AllowResult::Allows;
2155 }
2156 }
2157 AllowResult::DoesNotAllow
2158 }
2159}
2160
2161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2163enum AllowResult {
2164 Allows,
2165 DoesNotAllow,
2166}
2167
2168fn does_url_match_expression_in_origin_with_redirect_count(
2170 url: &Url,
2171 expression: &str,
2172 origin: &Origin,
2173 redirect_count: u32,
2174) -> MatchResult {
2175 let url_scheme = url.scheme();
2176 if expression == "*" {
2177 if scheme_is_network(url_scheme) {
2178 return Matches;
2179 }
2180 return origin_scheme_part_match(origin, url_scheme);
2181 }
2182 if let Some(captures) = SCHEME_SOURCE_GRAMMAR.captures(expression) {
2183 if let Some(expression_scheme) = captures.name("scheme") {
2184 return scheme_part_match(expression_scheme.as_str(), url_scheme);
2185 }
2186 return DoesNotMatch;
2188 }
2189 if let Some(captures) = HOST_SOURCE_GRAMMAR.captures(expression) {
2190 let expr_has_scheme_part = if let Some(expression_scheme) = captures.name("scheme") {
2191 if scheme_part_match(expression_scheme.as_str(), url_scheme) != Matches {
2192 return DoesNotMatch;
2193 }
2194 true
2195 } else {
2196 false
2197 };
2198 let url_host = if let Some(url_host) = url.host() {
2199 url_host
2200 } else {
2201 return DoesNotMatch;
2202 };
2203 if !expr_has_scheme_part && origin_scheme_part_match(origin, url.scheme()) != Matches {
2204 return DoesNotMatch;
2205 }
2206 if let Some(expression_host) = captures.name("host") {
2207 if host_part_match(expression_host.as_str(), &url_host.to_string()) != Matches {
2208 return DoesNotMatch;
2209 }
2210 } else {
2211 return DoesNotMatch;
2213 }
2214 let port_part = captures.name("port").map(|port| &port.as_str()[1..]);
2216 if port_part_match(port_part, url) != Matches {
2217 return DoesNotMatch;
2218 }
2219 let path_part = captures
2220 .name("path")
2221 .map(|path_part| path_part.as_str())
2222 .unwrap_or("");
2223 if path_part != "/" && redirect_count == 0 {
2224 let path = url.path();
2225 if path_part_match(path_part, path) != Matches {
2226 return DoesNotMatch;
2227 }
2228 }
2229 return Matches;
2230 }
2231 if ascii_case_insensitive_match(expression, "'self'") {
2232 if *origin == url.origin() {
2233 return Matches;
2234 }
2235 if let Origin::Tuple(scheme, host, port) = origin {
2236 let hosts_are_the_same = Some(host) == url.host().map(|p| p.to_owned()).as_ref();
2237 let ports_are_the_same = Some(*port) == url.port();
2238 let origins_port_is_default_for_scheme = Some(*port) == default_port(scheme);
2239 let url_port_is_default_port_for_scheme =
2240 url.port() == default_port(scheme) && default_port(scheme).is_some();
2241 let ports_are_default =
2242 url_port_is_default_port_for_scheme && origins_port_is_default_for_scheme;
2243 if hosts_are_the_same
2244 && (ports_are_the_same || ports_are_default)
2245 && ((url_scheme == "https" || url_scheme == "wss")
2246 || (scheme == "http" && (url_scheme == "http" || url_scheme == "ws")))
2247 {
2248 return Matches;
2249 }
2250 }
2251 }
2252 DoesNotMatch
2253}
2254
2255fn host_part_match(pattern: &str, host: &str) -> MatchResult {
2257 debug_assert!(!host.is_empty());
2258 if host.is_empty() {
2260 return DoesNotMatch;
2261 }
2262 if pattern.as_bytes()[0] == b'*' {
2263 if pattern.len() == 1 {
2265 return Matches;
2266 }
2267 if pattern.as_bytes()[1] == b'.' {
2269 let remaining_pattern = &pattern[1..];
2271 if remaining_pattern.len() > host.len() {
2272 return DoesNotMatch;
2273 }
2274 let remaining_host = &host[(host.len() - remaining_pattern.len())..];
2275 debug_assert_eq!(remaining_host.len(), remaining_pattern.len());
2276 if ascii_case_insensitive_match(remaining_pattern, remaining_host) {
2278 return Matches;
2279 }
2280 return DoesNotMatch;
2282 }
2283 }
2284 if !ascii_case_insensitive_match(pattern, host) {
2286 return DoesNotMatch;
2287 }
2288 static IPV4_ADDRESS_RULE: Lazy<Regex> = Lazy::new(|| {
2289 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()
2290 });
2291 if IPV4_ADDRESS_RULE.is_match(pattern) && pattern != "127.0.0.1" {
2292 return DoesNotMatch;
2293 }
2294 if pattern.as_bytes()[0] == b'[' {
2298 return DoesNotMatch;
2299 }
2300 Matches
2302}
2303
2304fn port_part_match(input: Option<&str>, url: &Url) -> MatchResult {
2306 use std::str::FromStr;
2307 debug_assert!(input.is_none() || input == Some("*") || u16::from_str(input.unwrap()).is_ok());
2309 if input == Some("*") {
2311 return Matches;
2312 }
2313 let normalized_input = if let Some(input) = input {
2315 u16::from_str(&input).ok()
2316 } else {
2317 None
2318 };
2319 if normalized_input == url.port() {
2321 return Matches;
2322 }
2323 if url.port().is_none() {
2325 let default_port = default_port(url.scheme());
2327 if normalized_input == default_port {
2329 return Matches;
2330 }
2331 }
2332 DoesNotMatch
2334}
2335
2336fn path_part_match(path_a: &str, path_b: &str) -> MatchResult {
2338 if path_a.is_empty() {
2339 return Matches;
2340 }
2341 if path_a == "/" && path_b.is_empty() {
2342 return Matches;
2343 }
2344 let exact_match = path_a.as_bytes()[path_a.len() - 1] != b'/';
2345 let (mut path_list_a, path_list_b): (Vec<&str>, Vec<&str>) =
2346 (path_a.split('/').collect(), path_b.split('/').collect());
2347 if path_list_a.len() > path_list_b.len() {
2348 return DoesNotMatch;
2349 }
2350 if exact_match && path_list_a.len() != path_list_b.len() {
2351 return DoesNotMatch;
2352 }
2353 if !exact_match {
2354 debug_assert_eq!(path_list_a[path_list_a.len() - 1], "");
2355 path_list_a.pop();
2356 }
2357 let mut piece_b_iter = path_list_b.iter();
2358 for piece_a in &path_list_a {
2359 let piece_b = piece_b_iter.next().unwrap();
2360 let piece_a: Vec<u8> = percent_encoding::percent_decode(piece_a.as_bytes()).collect();
2361 let piece_b: Vec<u8> = percent_encoding::percent_decode(piece_b.as_bytes()).collect();
2362 if piece_a != piece_b {
2363 return DoesNotMatch;
2364 }
2365 }
2366 Matches
2367}
2368
2369fn default_port(scheme: &str) -> Option<u16> {
2370 Some(match scheme {
2371 "ftp" => 21,
2372 "gopher" => 70,
2373 "http" => 80,
2374 "https" => 443,
2375 "ws" => 80,
2376 "wss" => 443,
2377 _ => return None,
2378 })
2379}
2380
2381fn origin_scheme_part_match(a: &Origin, b: &str) -> MatchResult {
2382 if let Origin::Tuple(scheme, _host, _port) = a {
2383 scheme_part_match(&scheme[..], b)
2384 } else {
2385 DoesNotMatch
2386 }
2387}
2388
2389fn scheme_part_match(a: &str, b: &str) -> MatchResult {
2391 let a = a.to_ascii_lowercase();
2392 let b = b.to_ascii_lowercase();
2393 match (&a[..], &b[..]) {
2394 _ if a == b => Matches,
2395 ("http", "https") | ("ws", "wss" | "http" | "https") | ("wss", "https") => Matches,
2396 _ => DoesNotMatch,
2397 }
2398}
2399
2400#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2401pub enum HashAlgorithm {
2402 Sha256,
2403 Sha384,
2404 Sha512,
2405}
2406
2407impl HashAlgorithm {
2408 pub fn from_name(name: &str) -> Option<HashAlgorithm> {
2409 use HashAlgorithm::*;
2410 match name {
2411 "sha256" | "Sha256" | "sHa256" | "shA256" | "SHa256" | "ShA256" | "sHA256"
2412 | "SHA256" => Some(Sha256),
2413 "sha384" | "Sha384" | "sHa384" | "shA384" | "SHa384" | "ShA384" | "sHA384"
2414 | "SHA384" => Some(Sha384),
2415 "sha512" | "Sha512" | "sHa512" | "shA512" | "SHa512" | "ShA512" | "sHA512"
2416 | "SHA512" => Some(Sha512),
2417 _ => None,
2418 }
2419 }
2420 pub fn apply(self, value: &str) -> String {
2421 use base64::Engine as _;
2422 let bytes = value.as_bytes();
2423 let standard = base64::engine::general_purpose::STANDARD;
2424 match self {
2425 HashAlgorithm::Sha256 => standard.encode(sha2::Sha256::digest(bytes)),
2426 HashAlgorithm::Sha384 => standard.encode(sha2::Sha384::digest(bytes)),
2427 HashAlgorithm::Sha512 => standard.encode(sha2::Sha512::digest(bytes)),
2428 }
2429 }
2430}
2431
2432#[derive(Clone, Debug, Eq, PartialEq)]
2434pub struct HashFunction {
2435 algorithm: HashAlgorithm,
2436 value: String,
2437 }
2439
2440#[derive(Clone, Debug, Eq, PartialEq)]
2442pub enum SubresourceIntegrityMetadata {
2443 NoMetadata,
2444 IntegritySources(Vec<HashFunction>),
2445}
2446
2447static SUBRESOURCE_METADATA_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
2450 Regex::new(r#"(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)"#)
2451 .unwrap()
2452});
2453
2454pub fn parse_subresource_integrity_metadata(string: &str) -> SubresourceIntegrityMetadata {
2456 let mut result = Vec::new();
2457 let mut empty = true;
2458 for token in split_ascii_whitespace(string) {
2459 empty = false;
2460 if let Some(captures) = SUBRESOURCE_METADATA_GRAMMAR.captures(token) {
2461 if let (Some(algorithm), Some(value)) = (
2462 captures
2463 .name("algorithm")
2464 .and_then(|a| HashAlgorithm::from_name(a.as_str())),
2465 captures.name("value"),
2466 ) {
2467 result.push(HashFunction {
2468 algorithm,
2469 value: String::from(value.as_str()),
2470 });
2471 }
2472 }
2473 }
2474 if empty {
2475 SubresourceIntegrityMetadata::NoMetadata
2476 } else {
2477 SubresourceIntegrityMetadata::IntegritySources(result)
2478 }
2479}
2480
2481#[cfg(test)]
2482mod test {
2483 use super::*;
2484 #[test]
2485 fn empty_directive_is_not_valid() {
2486 let d = Directive {
2487 name: String::new(),
2488 value: Vec::new(),
2489 };
2490 assert!(!d.is_valid());
2491 }
2492 #[test]
2493 pub fn duplicate_policy_is_not_valid() {
2494 let d = Directive {
2495 name: "test".to_owned(),
2496 value: vec!["test".to_owned()],
2497 };
2498 let p = Policy {
2499 directive_set: vec![d.clone(), d.clone()],
2500 disposition: PolicyDisposition::Enforce,
2501 source: PolicySource::Header,
2502 };
2503 assert!(!p.is_valid());
2504 }
2505 #[test]
2506 pub fn basic_policy_is_valid() {
2507 let p = Policy::parse(
2508 "script-src notriddle.com",
2509 PolicySource::Header,
2510 PolicyDisposition::Enforce,
2511 );
2512 assert!(p.is_valid());
2513 }
2514 #[test]
2515 pub fn policy_with_empty_directive_set_is_not_valid() {
2516 let p = Policy {
2517 directive_set: vec![],
2518 disposition: PolicyDisposition::Enforce,
2519 source: PolicySource::Header,
2520 };
2521 assert!(!p.is_valid());
2522 }
2523
2524 #[test]
2525 pub fn prefetch_request_does_not_violate_policy() {
2526 let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2527 let request = Request {
2528 url: url.clone(),
2529 current_url: url,
2530 origin: Origin::Tuple(
2531 "https".to_string(),
2532 url::Host::Domain("notriddle.com".to_owned()),
2533 443,
2534 ),
2535 redirect_count: 0,
2536 destination: Destination::Script,
2537 initiator: Initiator::Prefetch,
2538 nonce: String::new(),
2539 integrity_metadata: String::new(),
2540 parser_metadata: ParserMetadata::None,
2541 };
2542
2543 let p = Policy::parse(
2544 "child-src 'self'",
2545 PolicySource::Header,
2546 PolicyDisposition::Enforce,
2547 );
2548
2549 let violation_result = p.does_request_violate_policy(&request);
2550
2551 assert!(violation_result == Violates::DoesNotViolate);
2552 }
2553
2554 #[test]
2555 pub fn prefetch_request_violates_policy() {
2556 let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2557 let request = Request {
2558 url: url.clone(),
2559 current_url: url,
2560 origin: Origin::Tuple(
2561 "https".to_string(),
2562 url::Host::Domain("notriddle.com".to_owned()),
2563 443,
2564 ),
2565 redirect_count: 0,
2566 destination: Destination::ServiceWorker,
2567 initiator: Initiator::None,
2568 nonce: String::new(),
2569 integrity_metadata: String::new(),
2570 parser_metadata: ParserMetadata::None,
2571 };
2572
2573 let p = Policy::parse(
2574 "default-src 'none'; script-src 'self' ",
2575 PolicySource::Header,
2576 PolicyDisposition::Enforce,
2577 );
2578
2579 let violation_result = p.does_request_violate_policy(&request);
2580
2581 let expected_result = Violates::Directive(Directive {
2582 name: String::from("script-src"),
2583 value: vec![String::from("'self'")],
2584 });
2585
2586 assert!(violation_result == expected_result);
2587 }
2588
2589 #[test]
2590 pub fn prefetch_request_is_allowed_by_directive() {
2591 let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2592 let request = Request {
2593 url: url.clone(),
2594 current_url: url,
2595 origin: Origin::Tuple(
2596 "https".to_string(),
2597 url::Host::Domain("notriddle.com".to_owned()),
2598 443,
2599 ),
2600 redirect_count: 0,
2601 destination: Destination::Script,
2602 initiator: Initiator::Prefetch,
2603 nonce: String::new(),
2604 integrity_metadata: String::new(),
2605 parser_metadata: ParserMetadata::None,
2606 };
2607
2608 let p = Policy::parse(
2609 "default-src 'none'; child-src 'self'",
2610 PolicySource::Header,
2611 PolicyDisposition::Enforce,
2612 );
2613
2614 let violation_result = p.does_request_violate_policy(&request);
2615
2616 assert!(violation_result == Violates::DoesNotViolate);
2617 }
2618
2619 #[test]
2620 pub fn websocket_request_is_allowed_by_directive() {
2621 let url = Url::parse("https://www.notriddle.com/websocket").unwrap();
2622 let request = Request {
2623 url: url.clone(),
2624 current_url: url,
2625 origin: Origin::Tuple(
2626 "https".to_string(),
2627 url::Host::Domain("notriddle.com".to_owned()),
2628 443,
2629 ),
2630 redirect_count: 0,
2631 destination: Destination::None,
2632 initiator: Initiator::None,
2633 nonce: String::new(),
2634 integrity_metadata: String::new(),
2635 parser_metadata: ParserMetadata::None,
2636 };
2637
2638 let p = Policy::parse(
2639 "connect-src ws://www.notriddle.com/websocket",
2640 PolicySource::Header,
2641 PolicyDisposition::Enforce,
2642 );
2643
2644 let violation_result = p.does_request_violate_policy(&request);
2645
2646 assert!(violation_result == Violates::DoesNotViolate);
2647 }
2648
2649 #[test]
2650 pub fn trusted_type_policy_is_valid() {
2651 let p = Policy::parse(
2652 "trusted-types 'none'",
2653 PolicySource::Meta,
2654 PolicyDisposition::Enforce,
2655 );
2656 assert!(p.is_valid());
2657 assert_eq!(p.directive_set[0].value, vec!["'none'".to_owned()]);
2658 }
2659
2660 #[test]
2661 pub fn csp_list_is_valid() {
2662 let csp_list = CspList::parse(
2663 "default-src 'none'; child-src 'self', trusted-types 'none'",
2664 PolicySource::Meta,
2665 PolicyDisposition::Enforce,
2666 );
2667 assert!(csp_list.is_valid());
2668 assert_eq!(
2669 csp_list.0[1].directive_set[0].value,
2670 vec!["'none'".to_owned()]
2671 );
2672 }
2673
2674 #[test]
2675 pub fn no_trusted_types_specified_allows_all_policies() {
2676 let csp_list = CspList::parse(
2677 "default-src 'none'; child-src 'self'",
2678 PolicySource::Meta,
2679 PolicyDisposition::Enforce,
2680 );
2681 assert!(csp_list.is_valid());
2682 let (check_result, violations) =
2683 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2684 assert_eq!(check_result, CheckResult::Allowed);
2685 assert!(violations.is_empty());
2686 }
2687
2688 #[test]
2689 pub fn none_does_not_allow_for_any_policy() {
2690 let csp_list = CspList::parse(
2691 "trusted-types 'none'",
2692 PolicySource::Meta,
2693 PolicyDisposition::Enforce,
2694 );
2695 assert!(csp_list.is_valid());
2696 let (check_result, violations) =
2697 csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2698 assert!(check_result == CheckResult::Blocked);
2699 assert_eq!(violations.len(), 1);
2700 }
2701
2702 #[test]
2703 pub fn extra_none_allows_all_policies() {
2704 let csp_list = CspList::parse(
2705 "trusted-types some-policy 'none'",
2706 PolicySource::Meta,
2707 PolicyDisposition::Enforce,
2708 );
2709 assert!(csp_list.is_valid());
2710 let (check_result, violations) =
2711 csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2712 assert!(check_result == CheckResult::Allowed);
2713 assert!(violations.is_empty());
2714 }
2715
2716 #[test]
2717 pub fn explicit_policy_named_is_allowed() {
2718 let csp_list = CspList::parse(
2719 "trusted-types MyPolicy",
2720 PolicySource::Meta,
2721 PolicyDisposition::Enforce,
2722 );
2723 assert!(csp_list.is_valid());
2724 let (check_result, violations) =
2725 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2726 assert_eq!(check_result, CheckResult::Allowed);
2727 assert!(violations.is_empty());
2728 }
2729
2730 #[test]
2731 pub fn other_policy_name_is_blocked() {
2732 let csp_list = CspList::parse(
2733 "trusted-types MyPolicy",
2734 PolicySource::Meta,
2735 PolicyDisposition::Enforce,
2736 );
2737 assert!(csp_list.is_valid());
2738 let (check_result, violations) =
2739 csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2740 assert!(check_result == CheckResult::Blocked);
2741 assert_eq!(violations.len(), 1);
2742 }
2743
2744 #[test]
2745 pub fn invalid_characters_in_policy_name_is_blocked() {
2746 let csp_list = CspList::parse(
2747 "trusted-types My?Policy",
2748 PolicySource::Meta,
2749 PolicyDisposition::Enforce,
2750 );
2751 assert!(csp_list.is_valid());
2752 let (check_result, violations) =
2753 csp_list.is_trusted_type_policy_creation_allowed("My?Policy", &["My?Policy"]);
2754 assert!(check_result == CheckResult::Blocked);
2755 assert_eq!(violations.len(), 1);
2756 }
2757
2758 #[test]
2759 pub fn already_created_policy_is_blocked() {
2760 let csp_list = CspList::parse(
2761 "trusted-types MyPolicy",
2762 PolicySource::Meta,
2763 PolicyDisposition::Enforce,
2764 );
2765 assert!(csp_list.is_valid());
2766 let (check_result, violations) =
2767 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2768 assert!(check_result == CheckResult::Blocked);
2769 assert_eq!(violations.len(), 1);
2770 }
2771
2772 #[test]
2773 pub fn already_created_policy_is_allowed_with_allow_duplicates() {
2774 let csp_list = CspList::parse(
2775 "trusted-types MyPolicy 'allow-duplicates'",
2776 PolicySource::Meta,
2777 PolicyDisposition::Enforce,
2778 );
2779 assert!(csp_list.is_valid());
2780 let (check_result, violations) =
2781 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2782 assert!(check_result == CheckResult::Allowed);
2783 assert!(violations.is_empty());
2784 }
2785
2786 #[test]
2787 pub fn only_report_policy_issues_for_disposition_report() {
2788 let csp_list = CspList::parse(
2789 "trusted-types MyPolicy",
2790 PolicySource::Meta,
2791 PolicyDisposition::Report,
2792 );
2793 assert!(csp_list.is_valid());
2794 let (check_result, violations) =
2795 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2796 assert!(check_result == CheckResult::Allowed);
2797 assert_eq!(violations.len(), 1);
2798 }
2799
2800 #[test]
2801 pub fn wildcard_allows_all_policies() {
2802 let csp_list = CspList::parse(
2803 "trusted-types *",
2804 PolicySource::Meta,
2805 PolicyDisposition::Report,
2806 );
2807 assert!(csp_list.is_valid());
2808 let (check_result, violations) =
2809 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2810 assert!(check_result == CheckResult::Allowed);
2811 assert!(violations.is_empty());
2812 }
2813
2814 #[test]
2815 pub fn violation_has_correct_directive() {
2816 let csp_list = CspList::parse(
2817 "trusted-types MyPolicy",
2818 PolicySource::Meta,
2819 PolicyDisposition::Enforce,
2820 );
2821 assert!(csp_list.is_valid());
2822 let (check_result, violations) =
2823 csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2824 assert!(check_result == CheckResult::Blocked);
2825 assert_eq!(violations.len(), 1);
2826 assert_eq!(violations[0].directive, csp_list.0[0].directive_set[0]);
2827 }
2828
2829 #[test]
2830 pub fn long_policy_name_is_truncated() {
2831 let csp_list = CspList::parse(
2832 "trusted-types MyPolicy",
2833 PolicySource::Meta,
2834 PolicyDisposition::Enforce,
2835 );
2836 assert!(csp_list.is_valid());
2837 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed(
2838 "SuperLongPolicyNameThatExceeds40Characters",
2839 &[],
2840 );
2841 assert!(check_result == CheckResult::Blocked);
2842 assert_eq!(violations.len(), 1);
2843 assert!(
2844 matches!(&violations[0].resource, ViolationResource::TrustedTypePolicy { sample } if sample == "SuperLongPolicyNameThatExceeds40Characte")
2845 );
2846 }
2847}