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 {
121 directive_set: Vec::new(),
122 source,
123 disposition,
124 };
125 for token in serialized.split(';') {
130 let token = strip_leading_and_trailing_ascii_whitespace(token);
132 if token.is_empty() || !token.is_ascii() {
135 continue;
136 };
137 let (directive_name, token) =
140 collect_a_sequence_of_non_ascii_white_space_code_points(token);
141 let mut directive_name = directive_name.to_owned();
143 directive_name.make_ascii_lowercase();
144 if policy.contains_a_directive_whose_name_is(&directive_name) {
146 continue;
147 }
148 let directive_value = split_ascii_whitespace(token).map(String::from).collect();
150 policy.directive_set.push(Directive {
153 name: directive_name,
154 value: directive_value,
155 });
156 }
157 policy
159 }
160 pub fn contains_a_directive_whose_name_is(&self, directive_name: &str) -> bool {
161 self.directive_set.iter().any(|d| d.name == directive_name)
162 }
163 pub fn does_request_violate_policy(&self, request: &Request) -> Violates {
165 if request.initiator == Initiator::Prefetch {
166 return self.does_resource_hint_violate_policy(request);
167 }
168
169 let mut violates = Violates::DoesNotViolate;
170 for directive in &self.directive_set {
171 let result = directive.pre_request_check(request, self);
172 if result == CheckResult::Blocked {
173 violates = Violates::Directive(directive.clone());
174 }
175 }
176 violates
177 }
178
179 pub fn does_resource_hint_violate_policy(&self, request: &Request) -> Violates {
181 let default_directive = &self.directive_set.iter().find(|x| x.name == "default-src");
182
183 if default_directive.is_none() {
184 return Violates::DoesNotViolate;
185 }
186
187 for directive in &self.directive_set {
188 let result = directive.pre_request_check(request, self);
189 if result == CheckResult::Allowed {
190 return Violates::DoesNotViolate;
191 }
192 }
193
194 return Violates::Directive(default_directive.unwrap().clone());
195 }
196}
197
198#[derive(Clone, Debug)]
199#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
200pub struct CspList(pub Vec<Policy>);
202
203impl Display for CspList {
204 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
205 for (i, directive) in self.0.iter().enumerate() {
206 if i != 0 {
207 write!(f, ",")?;
208 }
209 <Policy as Display>::fmt(directive, f)?;
210 }
211 Ok(())
212 }
213}
214
215static TRUSTED_POLICY_SOURCE_GRAMMAR: Lazy<Regex> =
217 Lazy::new(|| Regex::new(r#"^[0-9a-zA-Z\-\#=_\/@\.%]+$"#).unwrap());
218
219impl CspList {
220 pub fn is_valid(&self) -> bool {
221 self.0.iter().all(Policy::is_valid)
222 }
223 pub fn contains_a_header_delivered_content_security_policy(&self) -> bool {
225 self.0
226 .iter()
227 .any(|policy| policy.source == PolicySource::Header)
228 }
229 pub fn parse(list: &str, source: PolicySource, disposition: PolicyDisposition) -> CspList {
231 let mut policies = Vec::new();
232 for token in split_commas(list) {
233 let policy = Policy::parse(token, source, disposition);
234 if policy.directive_set.is_empty() {
235 continue;
236 };
237 policies.push(policy)
238 }
239 CspList(policies)
240 }
241 pub fn append(&mut self, mut other: CspList) {
242 self.0.append(&mut other.0)
243 }
244 pub fn push(&mut self, policy: Policy) {
245 self.0.push(policy)
246 }
247 pub fn report_violations_for_request(&self, request: &Request) -> Vec<Violation> {
253 let mut violations = Vec::new();
254 for policy in &self.0 {
255 if policy.disposition == PolicyDisposition::Enforce {
256 continue;
257 };
258 let violates = policy.does_request_violate_policy(request);
259 if let Violates::Directive(directive) = violates {
260 let resource = ViolationResource::Url(request.url.clone());
261 violations.push(Violation {
262 resource,
263 directive: Directive {
264 name: get_the_effective_directive_for_request(request).to_owned(),
265 value: directive.value.clone(),
266 },
267 policy: policy.clone(),
268 });
269 }
270 }
271 violations
272 }
273 pub fn should_request_be_blocked(&self, request: &Request) -> (CheckResult, Vec<Violation>) {
280 let mut result = CheckResult::Allowed;
281 let mut violations = Vec::new();
282 for policy in &self.0 {
283 if policy.disposition == PolicyDisposition::Report {
284 continue;
285 };
286 let violates = policy.does_request_violate_policy(request);
287 if let Violates::Directive(directive) = violates {
288 result = CheckResult::Blocked;
289 let resource = ViolationResource::Url(request.url.clone());
290 violations.push(Violation {
291 resource,
292 directive: Directive {
293 name: get_the_effective_directive_for_request(request).to_owned(),
294 value: directive.value.clone(),
295 },
296 policy: policy.clone(),
297 });
298 }
299 }
300 (result, violations)
301 }
302 pub fn should_response_to_request_be_blocked(
309 &self,
310 request: &Request,
311 response: &Response,
312 ) -> (CheckResult, Vec<Violation>) {
313 let mut result = CheckResult::Allowed;
316 let mut violations = Vec::new();
317 for policy in &self.0 {
319 for directive in &policy.directive_set {
321 if directive.post_request_check(request, response, policy) == CheckResult::Blocked {
323 violations.push(Violation {
326 resource: ViolationResource::Url(request.url.clone()),
327 directive: Directive {
328 name: get_the_effective_directive_for_request(request).to_owned(),
329 value: directive.value.clone(),
330 },
331 policy: policy.clone(),
332 });
333 if policy.disposition == PolicyDisposition::Enforce {
335 result = CheckResult::Blocked;
336 }
337 }
338 }
339 }
340 (result, violations)
341 }
342 pub fn should_elements_inline_type_behavior_be_blocked(
344 &self,
345 element: &Element,
346 type_: InlineCheckType,
347 source: &str,
348 ) -> (CheckResult, Vec<Violation>) {
349 use CheckResult::*;
350 let mut result = Allowed;
351 let mut violations = Vec::new();
352 for policy in &self.0 {
353 for directive in &policy.directive_set {
354 if directive.inline_check(element, type_, policy, source) == Allowed {
355 continue;
356 }
357 let sample = if directive.value.iter().any(|t| &t[..] == "'report-sample'") {
358 let max_length = cmp::min(40, source.len());
359 Some(source[0..max_length].to_owned())
360 } else {
361 None
362 };
363 let violation = Violation {
364 resource: ViolationResource::Inline { sample },
365 directive: Directive {
366 name: get_the_effective_directive_for_inline_checks(type_).to_owned(),
367 value: directive.value.clone(),
368 },
369 policy: policy.clone(),
370 };
371 violations.push(violation);
372 if policy.disposition == PolicyDisposition::Enforce {
373 result = Blocked;
374 }
375 }
376 }
377 (result, violations)
378 }
379 pub fn is_base_allowed_for_document(
386 &self,
387 base: &Url,
388 self_origin: &Origin,
389 ) -> (CheckResult, Vec<Violation>) {
390 use CheckResult::*;
391 let mut violations = Vec::new();
392 for policy in &self.0 {
393 let directive = policy
394 .directive_set
395 .iter()
396 .find(|directive| directive.name == "base-uri");
397 if let Some(directive) = directive {
398 if SourceList(&directive.value)
399 .does_url_match_source_list_in_origin_with_redirect_count(base, &self_origin, 0)
400 == DoesNotMatch
401 {
402 let violation = Violation {
403 directive: directive.clone(),
404 resource: ViolationResource::Inline { sample: None },
405 policy: policy.clone(),
406 };
407 violations.push(violation);
408 if policy.disposition == PolicyDisposition::Enforce {
409 return (Blocked, violations);
410 }
411 }
412 }
413 }
414 return (Allowed, violations);
415 }
416
417 pub fn is_trusted_type_policy_creation_allowed(
424 &self,
425 policy_name: &str,
426 created_policy_names: &[&str],
427 ) -> (CheckResult, Vec<Violation>) {
428 use CheckResult::*;
429 let mut result = Allowed;
431 let mut violations = Vec::new();
432 for policy in &self.0 {
434 let mut create_violation = false;
436 let directive = policy
438 .directive_set
439 .iter()
440 .find(|directive| directive.name == "trusted-types");
441 if let Some(directive) = directive {
443 if directive.value.len() == 1 && directive.value.contains(&"'none'".to_string()) {
445 create_violation = true;
446 }
447 if created_policy_names.contains(&policy_name)
450 && !directive.value.iter().any(|v| v == "'allow-duplicates'")
451 {
452 create_violation = true;
453 }
454 if !(TRUSTED_POLICY_SOURCE_GRAMMAR.is_match(&policy_name)
457 && (directive.value.iter().any(|p| p == policy_name)
458 || directive.value.iter().any(|v| v == "*")))
459 {
460 create_violation = true;
461 }
462 if !create_violation {
464 continue;
465 }
466 let max_length = cmp::min(40, policy_name.len());
467 let sample = policy_name[0..max_length].to_owned();
469 let violation = Violation {
472 directive: directive.clone(),
473 resource: ViolationResource::TrustedTypePolicy {
475 sample,
477 },
478 policy: policy.clone(),
479 };
480 violations.push(violation);
482 if policy.disposition == PolicyDisposition::Enforce {
484 result = Blocked
485 }
486 }
487 }
488 return (result, violations);
489 }
490 pub fn does_sink_type_require_trusted_types(
497 &self,
498 sink_group: &str,
499 include_report_only_policies: bool,
500 ) -> bool {
501 let sink_group = &sink_group.to_owned();
502 for policy in &self.0 {
504 let directive = policy
506 .directive_set
507 .iter()
508 .find(|directive| directive.name == "require-trusted-types-for");
509 if let Some(directive) = directive {
511 if !directive.value.contains(sink_group) {
513 continue;
514 }
515 let enforced = policy.disposition == PolicyDisposition::Enforce;
517 if enforced {
519 return true;
520 }
521 if include_report_only_policies {
523 return true;
524 }
525 }
526 }
527 false
529 }
530 pub fn should_sink_type_mismatch_violation_be_blocked_by_csp(
537 &self,
538 sink: &str,
539 sink_group: &str,
540 source: &str,
541 ) -> (CheckResult, Vec<Violation>) {
542 use CheckResult::*;
543 let sink_group = &sink_group.to_owned();
544 let mut result = Allowed;
546 let mut violations = Vec::new();
547 let mut sample = source;
549 if sink == "Function" {
551 if sample.starts_with("function anonymous") {
553 sample = &sample[18..];
554 } else if sample.starts_with("async function anonymous") {
556 sample = &sample[24..];
557 } else if sample.starts_with("function* anonymous") {
559 sample = &sample[19..];
560 } else if sample.starts_with("async function* anonymous") {
562 sample = &sample[25..];
563 }
564 }
565 for policy in &self.0 {
567 let directive = policy
569 .directive_set
570 .iter()
571 .find(|directive| directive.name == "require-trusted-types-for");
572 let Some(directive) = directive else { continue };
574 if !directive.value.contains(sink_group) {
576 continue;
577 }
578 let mut trimmed_sample: String = sample.into();
580 trimmed_sample.truncate(40);
581 violations.push(Violation {
584 resource: ViolationResource::TrustedTypeSink {
586 sample: sink.to_owned() + "|" + &trimmed_sample,
588 },
589 directive: directive.clone(),
590 policy: policy.clone(),
591 });
592 if policy.disposition == PolicyDisposition::Enforce {
594 result = Blocked
595 }
596 }
597 (result, violations)
599 }
600 pub fn get_sandboxing_flag_set_for_document(&self) -> Option<SandboxingFlagSet> {
602 self.0
605 .iter()
606 .flat_map(|policy| {
607 policy
608 .directive_set
609 .iter()
610 .rev()
612 .find(|directive| directive.name == "sandbox")
615 .and_then(|directive| directive.get_sandboxing_flag_set_for_document(policy))
616 })
617 .next()
619 }
620 pub fn is_js_evaluation_allowed(&self, source: &str) -> (CheckResult, Vec<Violation>) {
622 let mut result = CheckResult::Allowed;
623 let mut violations = Vec::new();
624 for policy in &self.0 {
626 let directive = policy
628 .directive_set
629 .iter()
630 .find(|directive| directive.name == "script-src")
633 .or_else(|| {
636 policy
637 .directive_set
638 .iter()
639 .find(|directive| directive.name == "default-src")
640 });
641 let Some(directive) = directive else { continue };
643 let source_list = SourceList(&directive.value);
644 if source_list.does_a_source_list_allow_js_evaluation() == AllowResult::Allows {
645 continue;
646 }
647 let trusted_types_required =
650 self.does_sink_type_require_trusted_types("'script'", false);
651 if trusted_types_required
654 && directive
655 .value
656 .iter()
657 .any(|t| ascii_case_insensitive_match(&t[..], "'trusted-types-eval'"))
658 {
659 continue;
660 }
661 if directive
664 .value
665 .iter()
666 .any(|t| ascii_case_insensitive_match(&t[..], "'unsafe-eval'"))
667 {
668 continue;
669 }
670 let sample = if directive.value.iter().any(|t| &t[..] == "'report-sample'") {
673 let max_length = cmp::min(40, source.len());
674 Some(source[0..max_length].to_owned())
675 } else {
676 None
677 };
678 violations.push(Violation {
681 resource: ViolationResource::Eval { sample },
683 directive: directive.clone(),
684 policy: policy.clone(),
685 });
686 if policy.disposition == PolicyDisposition::Enforce {
688 result = CheckResult::Blocked
689 }
690 }
691 (result, violations)
692 }
693 pub fn is_wasm_evaluation_allowed(&self) -> (CheckResult, Vec<Violation>) {
695 let mut result = CheckResult::Allowed;
696 let mut violations = Vec::new();
697 for policy in &self.0 {
699 let directive = policy
701 .directive_set
702 .iter()
703 .find(|directive| directive.name == "script-src")
706 .or_else(|| {
709 policy
710 .directive_set
711 .iter()
712 .find(|directive| directive.name == "default-src")
713 });
714 let Some(directive) = directive else { continue };
715 let source_list = SourceList(&directive.value);
716 if source_list.does_a_source_list_allow_wasm_evaluation() == AllowResult::Allows {
721 continue;
722 }
723 violations.push(Violation {
726 resource: ViolationResource::WasmEval,
728 directive: directive.clone(),
729 policy: policy.clone(),
730 });
731 if policy.disposition == PolicyDisposition::Enforce {
733 result = CheckResult::Blocked
734 }
735 }
736 (result, violations)
737 }
738 pub fn should_navigation_request_be_blocked<TrustedTypesUrlProcessor>(
749 &self,
750 request: &mut Request,
751 navigation_check_type: NavigationCheckType,
752 mut url_processor: TrustedTypesUrlProcessor,
753 ) -> (CheckResult, Vec<Violation>)
754 where
755 TrustedTypesUrlProcessor: FnMut(&str) -> Option<String>,
756 {
757 let mut result = CheckResult::Allowed;
759 let mut violations = Vec::new();
760 for policy in &self.0 {
762 for directive in &policy.directive_set {
764 if directive.pre_navigation_check(
767 request,
768 navigation_check_type,
769 &mut url_processor,
770 policy,
771 ) == CheckResult::Allowed
772 {
773 continue;
774 }
775 violations.push(Violation {
779 resource: ViolationResource::Url(request.url.clone()),
781 directive: Directive {
782 name: get_the_effective_directive_for_request(request).to_owned(),
783 value: directive.value.clone(),
784 },
785 policy: policy.clone(),
786 });
787 if policy.disposition == PolicyDisposition::Enforce {
789 result = CheckResult::Blocked;
790 }
791 }
792 }
793 if result == CheckResult::Allowed && request.current_url.scheme() == "javascript" {
795 for policy in &self.0 {
797 for directive in &policy.directive_set {
799 if directive.inline_check(
802 &Element { nonce: None },
803 InlineCheckType::Navigation,
804 policy,
805 request.current_url.as_str(),
806 ) == CheckResult::Allowed
807 {
808 continue;
809 }
810 violations.push(Violation {
814 resource: ViolationResource::Inline { sample: None },
816 directive: Directive {
817 name: get_the_effective_directive_for_inline_checks(
820 InlineCheckType::Navigation,
821 )
822 .to_owned(),
823 value: directive.value.clone(),
824 },
825 policy: policy.clone(),
826 });
827 if policy.disposition == PolicyDisposition::Enforce {
829 result = CheckResult::Blocked;
830 }
831 }
832 }
833 }
834 (result, violations)
835 }
836 pub fn should_navigation_response_to_navigation_request_be_blocked(
838 &self,
839 response: &Response,
840 self_origin: &Origin,
841 parent_navigable_origins: &Vec<Url>,
842 ) -> (CheckResult, Vec<Violation>) {
843 let mut result = CheckResult::Allowed;
845 let mut violations = Vec::new();
846 for policy in &self.0 {
848 for directive in &policy.directive_set {
850 if directive.navigation_response_check(
854 response,
855 self_origin,
856 parent_navigable_origins,
857 policy,
858 ) == CheckResult::Allowed
859 {
860 continue;
861 }
862 violations.push(Violation {
865 resource: ViolationResource::Url(response.url.clone()),
867 directive: directive.clone(),
868 policy: policy.clone(),
869 });
870 if policy.disposition == PolicyDisposition::Enforce {
872 result = CheckResult::Blocked;
873 }
874 }
875 }
876 (result, violations)
880 }
881}
882
883#[derive(Clone, Debug)]
884pub struct Element<'a> {
885 pub nonce: Option<Cow<'a, str>>,
891}
892
893#[derive(Clone, Copy, Debug, Eq, PartialEq)]
899pub enum InlineCheckType {
900 Script,
901 ScriptAttribute,
902 Style,
903 StyleAttribute,
904 Navigation,
905}
906
907#[derive(Clone, Copy, Debug, Eq, PartialEq)]
913pub enum NavigationCheckType {
914 FormSubmission,
915 Other,
916}
917
918#[derive(Clone, Debug)]
924pub struct Request {
925 pub url: Url,
926 pub current_url: Url,
927 pub origin: Origin,
928 pub redirect_count: u32,
929 pub destination: Destination,
930 pub initiator: Initiator,
931 pub nonce: String,
932 pub integrity_metadata: String,
933 pub parser_metadata: ParserMetadata,
934}
935
936#[derive(Clone, Copy, Debug, Eq, PartialEq)]
937pub enum ParserMetadata {
938 ParserInserted,
939 NotParserInserted,
940 None,
941}
942
943#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
944#[derive(Clone, Copy, Debug, Eq, PartialEq)]
945pub enum Initiator {
946 Download,
947 ImageSet,
948 Manifest,
949 Prefetch,
950 Prerender,
951 Fetch,
952 Xslt,
953 None,
954}
955
956#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
957#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
958pub enum Destination {
959 None,
960 Audio,
961 AudioWorklet,
962 Document,
963 Embed,
964 Font,
965 Frame,
966 IFrame,
967 Image,
968 Json,
969 Manifest,
970 Object,
971 PaintWorklet,
972 Report,
973 Script,
974 ServiceWorker,
975 SharedWorker,
976 Style,
977 Track,
978 Video,
979 WebIdentity,
980 Worker,
981 Xslt,
982}
983
984pub struct InvalidDestination;
985
986impl FromStr for Destination {
987 type Err = InvalidDestination;
988
989 fn from_str(s: &str) -> Result<Self, Self::Err> {
990 let destination = match s {
991 "" => Self::None,
992 "audio" => Self::Audio,
993 "audioworklet" => Self::AudioWorklet,
994 "document" => Self::Document,
995 "embed" => Self::Embed,
996 "font" => Self::Font,
997 "frame" => Self::Frame,
998 "iframe" => Self::IFrame,
999 "image" => Self::Image,
1000 "json" => Self::Json,
1001 "manifest" => Self::Manifest,
1002 "object" => Self::Object,
1003 "paintworklet" => Self::PaintWorklet,
1004 "report" => Self::Report,
1005 "script" => Self::Script,
1006 "serviceworker" => Self::ServiceWorker,
1007 "sharedworker" => Self::SharedWorker,
1008 "style" => Self::Style,
1009 "track" => Self::Track,
1010 "video" => Self::Video,
1011 "webidentity" => Self::WebIdentity,
1012 "worker" => Self::Worker,
1013 "xslt" => Self::Xslt,
1014 _ => return Err(InvalidDestination),
1015 };
1016
1017 Ok(destination)
1018 }
1019}
1020
1021impl Destination {
1022 pub fn is_script_like(self) -> bool {
1024 use Destination::*;
1025 matches!(
1026 self,
1027 AudioWorklet | PaintWorklet | Script | ServiceWorker | SharedWorker | Worker | Xslt
1028 )
1029 }
1030
1031 pub const fn as_str(&self) -> &'static str {
1032 match self {
1033 Self::None => "",
1034 Self::Audio => "audio",
1035 Self::AudioWorklet => "audioworklet",
1036 Self::Document => "document",
1037 Self::Embed => "embed",
1038 Self::Font => "font",
1039 Self::Frame => "frame",
1040 Self::IFrame => "iframe",
1041 Self::Image => "image",
1042 Self::Json => "json",
1043 Self::Manifest => "manifest",
1044 Self::Object => "object",
1045 Self::PaintWorklet => "paintworklet",
1046 Self::Report => "report",
1047 Self::Script => "script",
1048 Self::ServiceWorker => "serviceworker",
1049 Self::SharedWorker => "sharedworker",
1050 Self::Style => "style",
1051 Self::Track => "track",
1052 Self::Video => "video",
1053 Self::WebIdentity => "webidentity",
1054 Self::Worker => "worker",
1055 Self::Xslt => "xslt",
1056 }
1057 }
1058}
1059
1060#[derive(Clone, Debug)]
1065pub struct Response {
1066 pub url: Url,
1067 pub redirect_count: u32,
1068}
1069
1070fn is_local_url(url: &Url) -> bool {
1072 let scheme = url.scheme();
1074 scheme == "about" || scheme == "blob" || scheme == "data"
1076}
1077
1078#[derive(Clone, Debug)]
1084#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1085pub struct Violation {
1086 pub resource: ViolationResource,
1087 pub directive: Directive,
1088 pub policy: Policy,
1089}
1090
1091#[derive(Clone, Debug, Eq, PartialEq)]
1097#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1098pub enum ViolationResource {
1099 Url(Url),
1100 Inline { sample: Option<String> },
1101 TrustedTypePolicy { sample: String },
1102 TrustedTypeSink { sample: String },
1103 Eval { sample: Option<String> },
1104 WasmEval,
1105}
1106
1107#[derive(Clone, Debug, Eq, PartialEq)]
1112#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1113pub enum CheckResult {
1114 Allowed,
1115 Blocked,
1116}
1117
1118#[derive(Clone, Debug, Eq, PartialEq)]
1122#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1123pub enum Violates {
1124 DoesNotViolate,
1125 Directive(Directive),
1126}
1127
1128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1130#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1131pub enum PolicyDisposition {
1132 Enforce,
1133 Report,
1134}
1135
1136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1138#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1139pub enum PolicySource {
1140 Header,
1141 Meta,
1142}
1143
1144#[derive(Clone, Debug, Eq, PartialEq)]
1146#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1147pub struct Directive {
1148 pub name: String,
1149 pub value: Vec<String>,
1150}
1151
1152impl Display for Directive {
1153 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
1154 <str as Display>::fmt(&self.name[..], f)?;
1155 write!(f, " ")?;
1156 for (i, token) in self.value.iter().enumerate() {
1157 if i != 0 {
1158 write!(f, " ")?;
1159 }
1160 <str as Display>::fmt(&token[..], f)?;
1161 }
1162 Ok(())
1163 }
1164}
1165
1166impl Directive {
1167 pub fn is_valid(&self) -> bool {
1169 DIRECTIVE_NAME_GRAMMAR.is_match(&self.name)
1170 && self
1171 .value
1172 .iter()
1173 .all(|t| DIRECTIVE_VALUE_TOKEN_GRAMMAR.is_match(&t[..]))
1174 }
1175 pub fn pre_request_check(&self, request: &Request, policy: &Policy) -> CheckResult {
1177 use CheckResult::*;
1178 match &self.name[..] {
1179 "child-src" => {
1180 let name = get_the_effective_directive_for_request(request);
1181 if !should_fetch_directive_execute(name, "child-src", policy) {
1182 return Allowed;
1183 }
1184 (Directive {
1185 name: String::from(name),
1186 value: self.value.clone(),
1187 })
1188 .pre_request_check(request, policy)
1189 }
1190 "connect-src" => {
1191 let name = get_the_effective_directive_for_request(request);
1192 if !should_fetch_directive_execute(name, "connect-src", policy) {
1193 return Allowed;
1194 }
1195 if SourceList(&self.value[..]).does_request_match_source_list(request)
1196 == DoesNotMatch
1197 {
1198 return Blocked;
1199 }
1200 Allowed
1201 }
1202 "default-src" => {
1203 let name = get_the_effective_directive_for_request(request);
1204 if !should_fetch_directive_execute(name, "default-src", policy) {
1205 return Allowed;
1206 }
1207 (Directive {
1208 name: String::from(name),
1209 value: self.value.clone(),
1210 })
1211 .pre_request_check(request, policy)
1212 }
1213 "font-src" => {
1214 let name = get_the_effective_directive_for_request(request);
1215 if !should_fetch_directive_execute(name, "font-src", policy) {
1216 return Allowed;
1217 }
1218 if SourceList(&self.value[..]).does_request_match_source_list(request)
1219 == DoesNotMatch
1220 {
1221 return Blocked;
1222 }
1223 Allowed
1224 }
1225 "frame-src" => {
1226 let name = get_the_effective_directive_for_request(request);
1227 if !should_fetch_directive_execute(name, "frame-src", policy) {
1228 return Allowed;
1229 }
1230 if SourceList(&self.value[..]).does_request_match_source_list(request)
1231 == DoesNotMatch
1232 {
1233 return Blocked;
1234 }
1235 Allowed
1236 }
1237 "img-src" => {
1238 let name = get_the_effective_directive_for_request(request);
1239 if !should_fetch_directive_execute(name, "img-src", policy) {
1240 return Allowed;
1241 }
1242 if SourceList(&self.value[..]).does_request_match_source_list(request)
1243 == DoesNotMatch
1244 {
1245 return Blocked;
1246 }
1247 Allowed
1248 }
1249 "manifest-src" => {
1250 let name = get_the_effective_directive_for_request(request);
1251 if !should_fetch_directive_execute(name, "manifest-src", policy) {
1252 return Allowed;
1253 }
1254 if SourceList(&self.value[..]).does_request_match_source_list(request)
1255 == DoesNotMatch
1256 {
1257 return Blocked;
1258 }
1259 Allowed
1260 }
1261 "media-src" => {
1262 let name = get_the_effective_directive_for_request(request);
1263 if !should_fetch_directive_execute(name, "media-src", policy) {
1264 return Allowed;
1265 }
1266 if SourceList(&self.value[..]).does_request_match_source_list(request)
1267 == DoesNotMatch
1268 {
1269 return Blocked;
1270 }
1271 Allowed
1272 }
1273 "object-src" => {
1274 let name = get_the_effective_directive_for_request(request);
1275 if !should_fetch_directive_execute(name, "object-src", policy) {
1276 return Allowed;
1277 }
1278 if SourceList(&self.value[..]).does_request_match_source_list(request)
1279 == DoesNotMatch
1280 {
1281 return Blocked;
1282 }
1283 Allowed
1284 }
1285 "script-src" => {
1286 let name = get_the_effective_directive_for_request(request);
1287 if !should_fetch_directive_execute(name, "script-src", policy) {
1288 return Allowed;
1289 }
1290 script_directives_prerequest_check(request, self)
1291 }
1292 "script-src-elem" => {
1293 let name = get_the_effective_directive_for_request(request);
1294 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1295 return Allowed;
1296 }
1297 script_directives_prerequest_check(request, self)
1298 }
1299 "style-src" => {
1300 let name = get_the_effective_directive_for_request(request);
1301 if !should_fetch_directive_execute(name, "style-src", policy) {
1302 return Allowed;
1303 }
1304 let source_list = SourceList(&self.value);
1305 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1306 return Allowed;
1307 }
1308 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1309 return Blocked;
1310 }
1311 Allowed
1312 }
1313 "style-src-elem" => {
1314 let name = get_the_effective_directive_for_request(request);
1315 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1316 return Allowed;
1317 }
1318 let source_list = SourceList(&self.value);
1319 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1320 return Allowed;
1321 }
1322 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1323 return Blocked;
1324 }
1325 Allowed
1326 }
1327 "worker-src" => {
1328 let name = get_the_effective_directive_for_request(request);
1329 if !should_fetch_directive_execute(name, "worker-src", policy) {
1330 return Allowed;
1331 }
1332 let source_list = SourceList(&self.value);
1333 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1334 return Blocked;
1335 }
1336 Allowed
1337 }
1338 _ => Allowed,
1339 }
1340 }
1341 pub fn post_request_check(
1343 &self,
1344 request: &Request,
1345 response: &Response,
1346 policy: &Policy,
1347 ) -> CheckResult {
1348 use CheckResult::*;
1349 match &self.name[..] {
1350 "child-src" => {
1351 let name = get_the_effective_directive_for_request(request);
1352 if !should_fetch_directive_execute(name, "child-src", policy) {
1353 return Allowed;
1354 }
1355 Directive {
1356 name: name.to_owned(),
1357 value: self.value.clone(),
1358 }
1359 .post_request_check(request, response, policy)
1360 }
1361 "connect-src" => {
1362 let name = get_the_effective_directive_for_request(request);
1363 if !should_fetch_directive_execute(name, "connect-src", policy) {
1364 return Allowed;
1365 }
1366 let source_list = SourceList(&self.value);
1367 if source_list.does_response_to_request_match_source_list(request, response)
1368 == DoesNotMatch
1369 {
1370 return Blocked;
1371 }
1372 Allowed
1373 }
1374 "default-src" => {
1375 let name = get_the_effective_directive_for_request(request);
1376 if !should_fetch_directive_execute(name, "default-src", policy) {
1377 return Allowed;
1378 }
1379 Directive {
1380 name: name.to_owned(),
1381 value: self.value.clone(),
1382 }
1383 .post_request_check(request, response, policy)
1384 }
1385 "font-src" => {
1386 let name = get_the_effective_directive_for_request(request);
1387 if !should_fetch_directive_execute(name, "font-src", policy) {
1388 return Allowed;
1389 }
1390 let source_list = SourceList(&self.value);
1391 if source_list.does_response_to_request_match_source_list(request, response)
1392 == DoesNotMatch
1393 {
1394 return Blocked;
1395 }
1396 Allowed
1397 }
1398 "frame-src" => {
1399 let name = get_the_effective_directive_for_request(request);
1400 if !should_fetch_directive_execute(name, "frame-src", policy) {
1401 return Allowed;
1402 }
1403 let source_list = SourceList(&self.value);
1404 if source_list.does_response_to_request_match_source_list(request, response)
1405 == DoesNotMatch
1406 {
1407 return Blocked;
1408 }
1409 Allowed
1410 }
1411 "img-src" => {
1412 let name = get_the_effective_directive_for_request(request);
1413 if !should_fetch_directive_execute(name, "img-src", policy) {
1414 return Allowed;
1415 }
1416 let source_list = SourceList(&self.value);
1417 if source_list.does_response_to_request_match_source_list(request, response)
1418 == DoesNotMatch
1419 {
1420 return Blocked;
1421 }
1422 Allowed
1423 }
1424 "manifest-src" => {
1425 let name = get_the_effective_directive_for_request(request);
1426 if !should_fetch_directive_execute(name, "manifest-src", policy) {
1427 return Allowed;
1428 }
1429 let source_list = SourceList(&self.value);
1430 if source_list.does_response_to_request_match_source_list(request, response)
1431 == DoesNotMatch
1432 {
1433 return Blocked;
1434 }
1435 Allowed
1436 }
1437 "media-src" => {
1438 let name = get_the_effective_directive_for_request(request);
1439 if !should_fetch_directive_execute(name, "media-src", policy) {
1440 return Allowed;
1441 }
1442 let source_list = SourceList(&self.value);
1443 if source_list.does_response_to_request_match_source_list(request, response)
1444 == DoesNotMatch
1445 {
1446 return Blocked;
1447 }
1448 Allowed
1449 }
1450 "object-src" => {
1451 let name = get_the_effective_directive_for_request(request);
1452 if !should_fetch_directive_execute(name, "object-src", policy) {
1453 return Allowed;
1454 }
1455 let source_list = SourceList(&self.value);
1456 if source_list.does_response_to_request_match_source_list(request, response)
1457 == DoesNotMatch
1458 {
1459 return Blocked;
1460 }
1461 Allowed
1462 }
1463 "script-src" => {
1464 let name = get_the_effective_directive_for_request(request);
1465 if !should_fetch_directive_execute(name, "script-src", policy) {
1466 return Allowed;
1467 }
1468 script_directives_postrequest_check(request, response, self)
1469 }
1470 "script-src-elem" => {
1471 let name = get_the_effective_directive_for_request(request);
1472 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1473 return Allowed;
1474 }
1475 script_directives_postrequest_check(request, response, self)
1476 }
1477 "style-src" => {
1478 let name = get_the_effective_directive_for_request(request);
1479 if !should_fetch_directive_execute(name, "style-src", policy) {
1480 return Allowed;
1481 }
1482 let source_list = SourceList(&self.value);
1483 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1484 return Allowed;
1485 }
1486 if source_list.does_response_to_request_match_source_list(request, response)
1487 == DoesNotMatch
1488 {
1489 return Blocked;
1490 }
1491 Allowed
1492 }
1493 "style-src-elem" => {
1494 let name = get_the_effective_directive_for_request(request);
1495 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1496 return Allowed;
1497 }
1498 let source_list = SourceList(&self.value);
1499 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1500 return Allowed;
1501 }
1502 if source_list.does_response_to_request_match_source_list(request, response)
1503 == DoesNotMatch
1504 {
1505 return Blocked;
1506 }
1507 Allowed
1508 }
1509 "worker-src" => {
1510 let name = get_the_effective_directive_for_request(request);
1511 if !should_fetch_directive_execute(name, "worker-src", policy) {
1512 return Allowed;
1513 }
1514 let source_list = SourceList(&self.value);
1515 if source_list.does_response_to_request_match_source_list(request, response)
1516 == DoesNotMatch
1517 {
1518 return Blocked;
1519 }
1520 Allowed
1521 }
1522 _ => Allowed,
1523 }
1524 }
1525 pub fn inline_check(
1527 &self,
1528 element: &Element,
1529 type_: InlineCheckType,
1530 policy: &Policy,
1531 source: &str,
1532 ) -> CheckResult {
1533 use CheckResult::*;
1534 match &self.name[..] {
1535 "default-src" => {
1536 let name = get_the_effective_directive_for_inline_checks(type_);
1537 if !should_fetch_directive_execute(name, "default-src", policy) {
1538 return Allowed;
1539 }
1540 Directive {
1541 name: name.to_owned(),
1542 value: self.value.clone(),
1543 }
1544 .inline_check(element, type_, policy, source)
1545 }
1546 "script-src" => {
1547 let name = get_the_effective_directive_for_inline_checks(type_);
1548 if !should_fetch_directive_execute(name, "script-src", policy) {
1549 return Allowed;
1550 }
1551 let source_list = SourceList(&self.value);
1552 if source_list
1553 .does_element_match_source_list_for_type_and_source(element, type_, source)
1554 == DoesNotMatch
1555 {
1556 return Blocked;
1557 }
1558 Allowed
1559 }
1560 "script-src-elem" => {
1561 let name = get_the_effective_directive_for_inline_checks(type_);
1562 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1563 return Allowed;
1564 }
1565 let source_list = SourceList(&self.value);
1566 if source_list
1567 .does_element_match_source_list_for_type_and_source(element, type_, source)
1568 == DoesNotMatch
1569 {
1570 return Blocked;
1571 }
1572 Allowed
1573 }
1574 "script-src-attr" => {
1575 let name = get_the_effective_directive_for_inline_checks(type_);
1576 if !should_fetch_directive_execute(name, "script-src-attr", policy) {
1577 return Allowed;
1578 }
1579 let source_list = SourceList(&self.value);
1580 if source_list
1581 .does_element_match_source_list_for_type_and_source(element, type_, source)
1582 == DoesNotMatch
1583 {
1584 return Blocked;
1585 }
1586 Allowed
1587 }
1588 "style-src" => {
1589 let name = get_the_effective_directive_for_inline_checks(type_);
1590 if !should_fetch_directive_execute(name, "style-src", policy) {
1591 return Allowed;
1592 }
1593 let source_list = SourceList(&self.value);
1594 if source_list
1595 .does_element_match_source_list_for_type_and_source(element, type_, source)
1596 == DoesNotMatch
1597 {
1598 return Blocked;
1599 }
1600 Allowed
1601 }
1602 "style-src-elem" => {
1603 let name = get_the_effective_directive_for_inline_checks(type_);
1604 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1605 return Allowed;
1606 }
1607 let source_list = SourceList(&self.value);
1608 if source_list
1609 .does_element_match_source_list_for_type_and_source(element, type_, source)
1610 == DoesNotMatch
1611 {
1612 return Blocked;
1613 }
1614 Allowed
1615 }
1616 "style-src-attr" => {
1617 let name = get_the_effective_directive_for_inline_checks(type_);
1618 if !should_fetch_directive_execute(name, "style-src-attr", policy) {
1619 return Allowed;
1620 }
1621 let source_list = SourceList(&self.value);
1622 if source_list
1623 .does_element_match_source_list_for_type_and_source(element, type_, source)
1624 == DoesNotMatch
1625 {
1626 return Blocked;
1627 }
1628 Allowed
1629 }
1630 _ => Allowed,
1631 }
1632 }
1633 pub fn get_sandboxing_flag_set_for_document(
1635 &self,
1636 policy: &Policy,
1637 ) -> Option<SandboxingFlagSet> {
1638 debug_assert!(&self.name[..] == "sandbox");
1639 if policy.disposition != PolicyDisposition::Enforce {
1641 None
1642 } else {
1643 Some(parse_a_sandboxing_directive(&self.value[..]))
1645 }
1646 }
1647 pub fn pre_navigation_check<TrustedTypesUrlProcessor>(
1649 &self,
1650 request: &mut Request,
1651 type_: NavigationCheckType,
1652 mut url_processor: TrustedTypesUrlProcessor,
1653 _policy: &Policy,
1654 ) -> CheckResult
1655 where
1656 TrustedTypesUrlProcessor: FnMut(&str) -> Option<String>,
1657 {
1658 use CheckResult::*;
1659 match &self.name[..] {
1660 "form-action" => {
1662 if type_ == NavigationCheckType::FormSubmission {
1664 let source_list = SourceList(&self.value);
1665 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1668 return Blocked;
1669 }
1670 }
1671 Allowed
1673 }
1674 "require-trusted-types-for" => {
1676 let url = &request.url;
1677 if url.scheme() != "javascript" {
1679 return Allowed;
1680 }
1681 let encoded_script_source = &url[Position::AfterScheme..][1..];
1686 let Some(converted_script_source) = url_processor(encoded_script_source) else {
1690 return Blocked;
1691 };
1692 let url_string = "javascript:".to_owned() + &converted_script_source;
1694 let Ok(new_url) = Url::parse(&url_string) else {
1697 return Blocked;
1698 };
1699 request.url = new_url;
1701 Allowed
1703 }
1704 _ => Allowed,
1705 }
1706 }
1707
1708 pub fn navigation_response_check(
1709 &self,
1710 response: &Response,
1711 self_origin: &Origin,
1712 parent_navigable_origins: &Vec<Url>,
1713 _policy: &Policy,
1714 ) -> CheckResult {
1715 use CheckResult::*;
1716 match &self.name[..] {
1717 "frame-ancestors" => {
1719 if is_local_url(&response.url) {
1721 return Allowed;
1722 }
1723 let source_list = SourceList(&self.value);
1732 for origin in parent_navigable_origins {
1736 if source_list.does_url_match_source_list_in_origin_with_redirect_count(
1741 origin,
1742 self_origin,
1743 0,
1744 ) == DoesNotMatch
1745 {
1746 return Blocked;
1747 }
1748 }
1750 Allowed
1752 }
1753 _ => Allowed,
1754 }
1755 }
1756}
1757
1758fn get_the_effective_directive_for_inline_checks(type_: InlineCheckType) -> &'static str {
1760 use InlineCheckType::*;
1761 match type_ {
1762 Script | Navigation => "script-src-elem",
1763 ScriptAttribute => "script-src-attr",
1764 Style => "style-src-elem",
1765 StyleAttribute => "style-src-attr",
1766 }
1767}
1768
1769fn script_directives_prerequest_check(request: &Request, directive: &Directive) -> CheckResult {
1771 use CheckResult::*;
1772 if request_is_script_like(request) {
1774 let source_list = SourceList(&directive.value[..]);
1775 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1778 return Allowed;
1779 }
1780 if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata)
1783 == Matches
1784 {
1785 return Allowed;
1786 }
1787 if directive
1790 .value
1791 .iter()
1792 .any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'"))
1793 {
1794 if request.parser_metadata == ParserMetadata::ParserInserted {
1796 return Blocked;
1797 }
1798 return Allowed;
1800 }
1801
1802 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1805 return Blocked;
1806 }
1807 }
1808 Allowed
1810}
1811
1812fn script_directives_postrequest_check(
1814 request: &Request,
1815 response: &Response,
1816 directive: &Directive,
1817) -> CheckResult {
1818 use CheckResult::*;
1819 if request_is_script_like(request) {
1821 let source_list = SourceList(&directive.value[..]);
1824 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1827 return Allowed;
1828 }
1829 if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata)
1832 == Matches
1833 {
1834 return Allowed;
1835 }
1836 if directive
1838 .value
1839 .iter()
1840 .any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'"))
1841 {
1842 if request.parser_metadata == ParserMetadata::ParserInserted {
1844 return Blocked;
1845 }
1846 return Allowed;
1848 }
1849 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch
1852 {
1853 return Blocked;
1854 }
1855 }
1856 Allowed
1858}
1859
1860fn request_is_script_like(request: &Request) -> bool {
1862 request.destination.is_script_like()
1863}
1864
1865fn should_fetch_directive_execute(
1867 effective_directive_name: &str,
1868 directive_name: &str,
1869 policy: &Policy,
1870) -> bool {
1871 let directive_fallback_list = get_fetch_directive_fallback_list(effective_directive_name);
1872 for fallback_directive in directive_fallback_list {
1873 if directive_name == *fallback_directive {
1874 return true;
1875 }
1876 if policy.contains_a_directive_whose_name_is(fallback_directive) {
1877 return false;
1878 }
1879 }
1880 false
1881}
1882
1883fn get_fetch_directive_fallback_list(directive_name: &str) -> &'static [&'static str] {
1885 match directive_name {
1886 "script-src-elem" => &["script-src-elem", "script-src", "default-src"],
1887 "script-src-attr" => &["script-src-attr", "script-src", "default-src"],
1888 "style-src-elem" => &["style-src-elem", "style-src", "default-src"],
1889 "style-src-attr" => &["style-src-attr", "style-src", "default-src"],
1890 "worker-src" => &["worker-src", "child-src", "script-src", "default-src"],
1891 "connect-src" => &["connect-src", "default-src"],
1892 "manifest-src" => &["manifest-src", "default-src"],
1893 "object-src" => &["object-src", "default-src"],
1894 "frame-src" => &["frame-src", "child-src", "default-src"],
1895 "media-src" => &["media-src", "default-src"],
1896 "font-src" => &["font-src", "default-src"],
1897 "img-src" => &["img-src", "default-src"],
1898 _ => &[],
1899 }
1900}
1901
1902fn get_the_effective_directive_for_request(request: &Request) -> &'static str {
1904 use Destination::*;
1905 use Initiator::*;
1906 if request.initiator == Prefetch || request.initiator == Prerender {
1908 return "default-src";
1909 }
1910 match request.destination {
1912 Destination::Manifest => "manifest-src",
1913 Object | Embed => "object-src",
1914 Frame | IFrame => "frame-src",
1915 Audio | Track | Video => "media-src",
1916 Font => "font-src",
1917 Image => "img-src",
1918 Style => "style-src-elem",
1919 Script | Destination::Xslt | AudioWorklet | PaintWorklet => "script-src-elem",
1920 ServiceWorker | SharedWorker | Worker => "worker-src",
1921 Json | WebIdentity => "connect-src",
1922 Report => "",
1923 _ => "connect-src",
1925 }
1926}
1927
1928#[derive(Clone, Debug, Eq, PartialEq)]
1930pub enum MatchResult {
1931 Matches,
1932 DoesNotMatch,
1933}
1934
1935static DIRECTIVE_NAME_GRAMMAR: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^[0-9a-z\-]+$"#).unwrap());
1937static DIRECTIVE_VALUE_TOKEN_GRAMMAR: Lazy<Regex> =
1939 Lazy::new(|| Regex::new(r#"^[\u{21}-\u{2B}\u{2D}-\u{3A}\u{3C}-\u{7E}]+$"#).unwrap());
1940static NONCE_SOURCE_GRAMMAR: Lazy<Regex> =
1942 Lazy::new(|| Regex::new(r#"^'nonce-(?P<n>[a-zA-Z0-9\+/\-_]+=*)'$"#).unwrap());
1943static NONE_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^'none'$"#).unwrap());
1944static SCHEME_SOURCE_GRAMMAR: Lazy<Regex> =
1946 Lazy::new(|| Regex::new(r#"^(?P<scheme>[a-zA-Z][a-zA-Z0-9\+\-\.]*):$"#).unwrap());
1947static HOST_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
1949 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()
1951});
1952static HASH_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
1954 Regex::new(r#"^'(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)'$"#)
1955 .unwrap()
1956});
1957
1958#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1960struct SourceList<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item = &'a U>>(I);
1961
1962impl<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item = &'a U>> SourceList<'a, U, I> {
1963 fn does_nonce_match_source_list(&self, nonce: &str) -> MatchResult {
1965 if nonce.is_empty() {
1966 return DoesNotMatch;
1967 };
1968 for expression in self.0.clone().into_iter() {
1969 if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression.borrow()) {
1970 if let Some(captured_nonce) = captures.name("n") {
1971 if nonce == captured_nonce.as_str() {
1972 return Matches;
1973 }
1974 }
1975 }
1976 }
1977 DoesNotMatch
1978 }
1979 fn does_integrity_metadata_match_source_list(&self, integrity_metadata: &str) -> MatchResult {
1981 let integrity_expressions: Vec<HashFunction> = self
1983 .0
1984 .clone()
1985 .into_iter()
1986 .filter_map(|expression| {
1987 if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression.borrow()) {
1988 if let (Some(algorithm), Some(value)) = (
1989 captures
1990 .name("algorithm")
1991 .and_then(|a| HashAlgorithm::from_name(a.as_str())),
1992 captures.name("value"),
1993 ) {
1994 return Some(HashFunction {
1995 algorithm,
1996 value: String::from(value.as_str()),
1997 });
1998 }
1999 }
2000 None
2001 })
2002 .collect();
2003 if integrity_expressions.is_empty() {
2005 return DoesNotMatch;
2006 }
2007 let integrity_sources = parse_subresource_integrity_metadata(integrity_metadata);
2009 match integrity_sources {
2010 SubresourceIntegrityMetadata::NoMetadata => DoesNotMatch,
2012 SubresourceIntegrityMetadata::IntegritySources(integrity_sources) => {
2013 if integrity_sources.is_empty() {
2014 return DoesNotMatch;
2015 }
2016 for source in &integrity_sources {
2018 if !integrity_expressions.iter().any(|ex| ex == source) {
2025 return DoesNotMatch;
2026 }
2027 }
2028 Matches
2030 }
2031 }
2032 }
2033 fn does_request_match_source_list(&self, request: &Request) -> MatchResult {
2035 self.does_url_match_source_list_in_origin_with_redirect_count(
2040 &request.current_url,
2041 &request.origin,
2042 request.redirect_count,
2043 )
2044 }
2045 fn does_url_match_source_list_in_origin_with_redirect_count(
2047 &self,
2048 url: &Url,
2049 origin: &Origin,
2050 redirect_count: u32,
2051 ) -> MatchResult {
2052 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2053 if NONE_SOURCE_GRAMMAR.is_match(expression) {
2054 continue;
2055 };
2056 let result = does_url_match_expression_in_origin_with_redirect_count(
2057 url,
2058 expression,
2059 origin,
2060 redirect_count,
2061 );
2062 if result == Matches {
2063 return Matches;
2064 }
2065 }
2066 DoesNotMatch
2067 }
2068 fn does_element_match_source_list_for_type_and_source(
2070 &self,
2071 element: &Element,
2072 type_: InlineCheckType,
2073 source: &str,
2074 ) -> MatchResult {
2075 if self.does_a_source_list_allow_all_inline_behavior_for_type(type_) == AllowResult::Allows
2076 {
2077 return Matches;
2078 }
2079 if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style {
2080 if let Some(nonce) = element.nonce.as_ref() {
2081 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2082 if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression) {
2083 if let Some(captured_nonce) = captures.name("n") {
2084 if nonce == captured_nonce.as_str() {
2085 return Matches;
2086 }
2087 }
2088 }
2089 }
2090 }
2091 }
2092 let mut unsafe_hashes = false;
2093 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2094 if ascii_case_insensitive_match(expression, "'unsafe-hashes'") {
2095 unsafe_hashes = true;
2096 break;
2097 }
2098 }
2099 if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style || unsafe_hashes {
2100 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2101 if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression) {
2102 if let (Some(algorithm), Some(value)) = (
2103 captures
2104 .name("algorithm")
2105 .and_then(|a| HashAlgorithm::from_name(a.as_str())),
2106 captures.name("value"),
2107 ) {
2108 let actual = algorithm.apply(source);
2109 let expected = value.as_str().replace('-', "+").replace('_', "/");
2110 if actual == expected {
2111 return Matches;
2112 }
2113 }
2114 }
2115 }
2116 }
2117 DoesNotMatch
2118 }
2119 fn does_a_source_list_allow_all_inline_behavior_for_type(
2121 &self,
2122 type_: InlineCheckType,
2123 ) -> AllowResult {
2124 use InlineCheckType::*;
2125 let mut allow_all_inline = false;
2126 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2127 if HASH_SOURCE_GRAMMAR.is_match(expression) || NONCE_SOURCE_GRAMMAR.is_match(expression)
2128 {
2129 return AllowResult::DoesNotAllow;
2130 }
2131 if (type_ == Script || type_ == ScriptAttribute || type_ == Navigation)
2132 && expression == "'strict-dynamic'"
2133 {
2134 return AllowResult::DoesNotAllow;
2135 }
2136 if ascii_case_insensitive_match(expression, "'unsafe-inline'") {
2137 allow_all_inline = true;
2138 }
2139 }
2140 if allow_all_inline {
2141 AllowResult::Allows
2142 } else {
2143 AllowResult::DoesNotAllow
2144 }
2145 }
2146 fn does_response_to_request_match_source_list(
2148 &self,
2149 request: &Request,
2150 response: &Response,
2151 ) -> MatchResult {
2152 self.does_url_match_source_list_in_origin_with_redirect_count(
2153 &response.url,
2154 &request.origin,
2155 response.redirect_count,
2156 )
2157 }
2158 fn does_a_source_list_allow_js_evaluation(&self) -> AllowResult {
2160 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2161 if ascii_case_insensitive_match(expression, "'unsafe-eval'") {
2164 return AllowResult::Allows;
2165 }
2166 }
2167 AllowResult::DoesNotAllow
2168 }
2169 fn does_a_source_list_allow_wasm_evaluation(&self) -> AllowResult {
2171 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2172 if ascii_case_insensitive_match(expression, "'unsafe-eval'")
2173 || ascii_case_insensitive_match(expression, "'wasm-unsafe-eval'")
2174 {
2175 return AllowResult::Allows;
2176 }
2177 }
2178 AllowResult::DoesNotAllow
2179 }
2180}
2181
2182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2184enum AllowResult {
2185 Allows,
2186 DoesNotAllow,
2187}
2188
2189fn does_url_match_expression_in_origin_with_redirect_count(
2191 url: &Url,
2192 expression: &str,
2193 origin: &Origin,
2194 redirect_count: u32,
2195) -> MatchResult {
2196 let url_scheme = url.scheme();
2197 if expression == "*" {
2198 if scheme_is_network(url_scheme) {
2199 return Matches;
2200 }
2201 return origin_scheme_part_match(origin, url_scheme);
2202 }
2203 if let Some(captures) = SCHEME_SOURCE_GRAMMAR.captures(expression) {
2204 if let Some(expression_scheme) = captures.name("scheme") {
2205 return scheme_part_match(expression_scheme.as_str(), url_scheme);
2206 }
2207 return DoesNotMatch;
2209 }
2210 if let Some(captures) = HOST_SOURCE_GRAMMAR.captures(expression) {
2211 let expr_has_scheme_part = if let Some(expression_scheme) = captures.name("scheme") {
2212 if scheme_part_match(expression_scheme.as_str(), url_scheme) != Matches {
2213 return DoesNotMatch;
2214 }
2215 true
2216 } else {
2217 false
2218 };
2219 let url_host = if let Some(url_host) = url.host() {
2220 url_host
2221 } else {
2222 return DoesNotMatch;
2223 };
2224 if !expr_has_scheme_part && origin_scheme_part_match(origin, url.scheme()) != Matches {
2225 return DoesNotMatch;
2226 }
2227 if let Some(expression_host) = captures.name("host") {
2228 if host_part_match(expression_host.as_str(), &url_host.to_string()) != Matches {
2229 return DoesNotMatch;
2230 }
2231 } else {
2232 return DoesNotMatch;
2234 }
2235 let port_part = captures.name("port").map(|port| &port.as_str()[1..]);
2237 if port_part_match(port_part, url) != Matches {
2238 return DoesNotMatch;
2239 }
2240 let path_part = captures
2241 .name("path")
2242 .map(|path_part| path_part.as_str())
2243 .unwrap_or("");
2244 if path_part != "/" && redirect_count == 0 {
2245 let path = url.path();
2246 if path_part_match(path_part, path) != Matches {
2247 return DoesNotMatch;
2248 }
2249 }
2250 return Matches;
2251 }
2252 if ascii_case_insensitive_match(expression, "'self'") {
2253 if *origin == url.origin() {
2254 return Matches;
2255 }
2256 if let Origin::Tuple(scheme, host, port) = origin {
2257 let hosts_are_the_same = Some(host) == url.host().map(|p| p.to_owned()).as_ref();
2258 let ports_are_the_same = Some(*port) == url.port();
2259 let origins_port_is_default_for_scheme = Some(*port) == default_port(scheme);
2260 let url_port_is_default_port_for_scheme =
2261 url.port() == default_port(scheme) && default_port(scheme).is_some();
2262 let ports_are_default =
2263 url_port_is_default_port_for_scheme && origins_port_is_default_for_scheme;
2264 if hosts_are_the_same
2265 && (ports_are_the_same || ports_are_default)
2266 && ((url_scheme == "https" || url_scheme == "wss")
2267 || (scheme == "http" && (url_scheme == "http" || url_scheme == "ws")))
2268 {
2269 return Matches;
2270 }
2271 }
2272 }
2273 DoesNotMatch
2274}
2275
2276fn host_part_match(pattern: &str, host: &str) -> MatchResult {
2278 debug_assert!(!host.is_empty());
2279 if host.is_empty() {
2281 return DoesNotMatch;
2282 }
2283 if pattern.as_bytes()[0] == b'*' {
2284 if pattern.len() == 1 {
2286 return Matches;
2287 }
2288 if pattern.as_bytes()[1] == b'.' {
2290 let remaining_pattern = &pattern[1..];
2292 if remaining_pattern.len() > host.len() {
2293 return DoesNotMatch;
2294 }
2295 let remaining_host = &host[(host.len() - remaining_pattern.len())..];
2296 debug_assert_eq!(remaining_host.len(), remaining_pattern.len());
2297 if ascii_case_insensitive_match(remaining_pattern, remaining_host) {
2299 return Matches;
2300 }
2301 return DoesNotMatch;
2303 }
2304 }
2305 if !ascii_case_insensitive_match(pattern, host) {
2307 return DoesNotMatch;
2308 }
2309 static IPV4_ADDRESS_RULE: Lazy<Regex> = Lazy::new(|| {
2310 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()
2311 });
2312 if IPV4_ADDRESS_RULE.is_match(pattern) && pattern != "127.0.0.1" {
2313 return DoesNotMatch;
2314 }
2315 if pattern.as_bytes()[0] == b'[' {
2319 return DoesNotMatch;
2320 }
2321 Matches
2323}
2324
2325fn port_part_match(input: Option<&str>, url: &Url) -> MatchResult {
2327 use std::str::FromStr;
2328 debug_assert!(input.is_none() || input == Some("*") || u16::from_str(input.unwrap()).is_ok());
2330 if input == Some("*") {
2332 return Matches;
2333 }
2334 let normalized_input = if let Some(input) = input {
2336 u16::from_str(&input).ok()
2337 } else {
2338 None
2339 };
2340 if normalized_input == url.port() {
2342 return Matches;
2343 }
2344 if url.port().is_none() {
2346 let default_port = default_port(url.scheme());
2348 if normalized_input == default_port {
2350 return Matches;
2351 }
2352 }
2353 DoesNotMatch
2355}
2356
2357fn path_part_match(path_a: &str, path_b: &str) -> MatchResult {
2359 if path_a.is_empty() {
2360 return Matches;
2361 }
2362 if path_a == "/" && path_b.is_empty() {
2363 return Matches;
2364 }
2365 let exact_match = path_a.as_bytes()[path_a.len() - 1] != b'/';
2366 let (mut path_list_a, path_list_b): (Vec<&str>, Vec<&str>) =
2367 (path_a.split('/').collect(), path_b.split('/').collect());
2368 if path_list_a.len() > path_list_b.len() {
2369 return DoesNotMatch;
2370 }
2371 if exact_match && path_list_a.len() != path_list_b.len() {
2372 return DoesNotMatch;
2373 }
2374 if !exact_match {
2375 debug_assert_eq!(path_list_a[path_list_a.len() - 1], "");
2376 path_list_a.pop();
2377 }
2378 let mut piece_b_iter = path_list_b.iter();
2379 for piece_a in &path_list_a {
2380 let piece_b = piece_b_iter.next().unwrap();
2381 let piece_a: Vec<u8> = percent_encoding::percent_decode(piece_a.as_bytes()).collect();
2382 let piece_b: Vec<u8> = percent_encoding::percent_decode(piece_b.as_bytes()).collect();
2383 if piece_a != piece_b {
2384 return DoesNotMatch;
2385 }
2386 }
2387 Matches
2388}
2389
2390fn default_port(scheme: &str) -> Option<u16> {
2391 Some(match scheme {
2392 "ftp" => 21,
2393 "gopher" => 70,
2394 "http" => 80,
2395 "https" => 443,
2396 "ws" => 80,
2397 "wss" => 443,
2398 _ => return None,
2399 })
2400}
2401
2402fn origin_scheme_part_match(a: &Origin, b: &str) -> MatchResult {
2403 if let Origin::Tuple(scheme, _host, _port) = a {
2404 scheme_part_match(&scheme[..], b)
2405 } else {
2406 DoesNotMatch
2407 }
2408}
2409
2410fn scheme_part_match(a: &str, b: &str) -> MatchResult {
2412 let a = a.to_ascii_lowercase();
2413 let b = b.to_ascii_lowercase();
2414 match (&a[..], &b[..]) {
2415 _ if a == b => Matches,
2416 ("http", "https") | ("ws", "wss" | "http" | "https") | ("wss", "https") => Matches,
2417 _ => DoesNotMatch,
2418 }
2419}
2420
2421#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2422pub enum HashAlgorithm {
2423 Sha256,
2424 Sha384,
2425 Sha512,
2426}
2427
2428impl HashAlgorithm {
2429 pub fn from_name(name: &str) -> Option<HashAlgorithm> {
2430 use HashAlgorithm::*;
2431 match name {
2432 "sha256" | "Sha256" | "sHa256" | "shA256" | "SHa256" | "ShA256" | "sHA256"
2433 | "SHA256" => Some(Sha256),
2434 "sha384" | "Sha384" | "sHa384" | "shA384" | "SHa384" | "ShA384" | "sHA384"
2435 | "SHA384" => Some(Sha384),
2436 "sha512" | "Sha512" | "sHa512" | "shA512" | "SHa512" | "ShA512" | "sHA512"
2437 | "SHA512" => Some(Sha512),
2438 _ => None,
2439 }
2440 }
2441 pub fn apply(self, value: &str) -> String {
2442 use base64::Engine as _;
2443 let bytes = value.as_bytes();
2444 let standard = base64::engine::general_purpose::STANDARD;
2445 match self {
2446 HashAlgorithm::Sha256 => standard.encode(sha2::Sha256::digest(bytes)),
2447 HashAlgorithm::Sha384 => standard.encode(sha2::Sha384::digest(bytes)),
2448 HashAlgorithm::Sha512 => standard.encode(sha2::Sha512::digest(bytes)),
2449 }
2450 }
2451}
2452
2453#[derive(Clone, Debug, Eq, PartialEq)]
2455pub struct HashFunction {
2456 algorithm: HashAlgorithm,
2457 value: String,
2458 }
2460
2461#[derive(Clone, Debug, Eq, PartialEq)]
2463pub enum SubresourceIntegrityMetadata {
2464 NoMetadata,
2465 IntegritySources(Vec<HashFunction>),
2466}
2467
2468static SUBRESOURCE_METADATA_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
2471 Regex::new(r#"(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)"#)
2472 .unwrap()
2473});
2474
2475pub fn parse_subresource_integrity_metadata(string: &str) -> SubresourceIntegrityMetadata {
2477 let mut result = Vec::new();
2478 let mut empty = true;
2479 for token in split_ascii_whitespace(string) {
2480 empty = false;
2481 if let Some(captures) = SUBRESOURCE_METADATA_GRAMMAR.captures(token) {
2482 if let (Some(algorithm), Some(value)) = (
2483 captures
2484 .name("algorithm")
2485 .and_then(|a| HashAlgorithm::from_name(a.as_str())),
2486 captures.name("value"),
2487 ) {
2488 result.push(HashFunction {
2489 algorithm,
2490 value: String::from(value.as_str()),
2491 });
2492 }
2493 }
2494 }
2495 if empty {
2496 SubresourceIntegrityMetadata::NoMetadata
2497 } else {
2498 SubresourceIntegrityMetadata::IntegritySources(result)
2499 }
2500}
2501
2502#[cfg(test)]
2503mod test {
2504 use super::*;
2505 #[test]
2506 fn empty_directive_is_not_valid() {
2507 let d = Directive {
2508 name: String::new(),
2509 value: Vec::new(),
2510 };
2511 assert!(!d.is_valid());
2512 }
2513 #[test]
2514 pub fn duplicate_policy_is_not_valid() {
2515 let d = Directive {
2516 name: "test".to_owned(),
2517 value: vec!["test".to_owned()],
2518 };
2519 let p = Policy {
2520 directive_set: vec![d.clone(), d.clone()],
2521 disposition: PolicyDisposition::Enforce,
2522 source: PolicySource::Header,
2523 };
2524 assert!(!p.is_valid());
2525 }
2526 #[test]
2527 pub fn basic_policy_is_valid() {
2528 let p = Policy::parse(
2529 "script-src notriddle.com",
2530 PolicySource::Header,
2531 PolicyDisposition::Enforce,
2532 );
2533 assert!(p.is_valid());
2534 }
2535 #[test]
2536 pub fn policy_with_empty_directive_set_is_not_valid() {
2537 let p = Policy {
2538 directive_set: vec![],
2539 disposition: PolicyDisposition::Enforce,
2540 source: PolicySource::Header,
2541 };
2542 assert!(!p.is_valid());
2543 }
2544
2545 #[test]
2546 pub fn prefetch_request_does_not_violate_policy() {
2547 let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2548 let request = Request {
2549 url: url.clone(),
2550 current_url: url,
2551 origin: Origin::Tuple(
2552 "https".to_string(),
2553 url::Host::Domain("notriddle.com".to_owned()),
2554 443,
2555 ),
2556 redirect_count: 0,
2557 destination: Destination::Script,
2558 initiator: Initiator::Prefetch,
2559 nonce: String::new(),
2560 integrity_metadata: String::new(),
2561 parser_metadata: ParserMetadata::None,
2562 };
2563
2564 let p = Policy::parse(
2565 "child-src 'self'",
2566 PolicySource::Header,
2567 PolicyDisposition::Enforce,
2568 );
2569
2570 let violation_result = p.does_request_violate_policy(&request);
2571
2572 assert!(violation_result == Violates::DoesNotViolate);
2573 }
2574
2575 #[test]
2576 pub fn prefetch_request_violates_policy() {
2577 let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2578 let request = Request {
2579 url: url.clone(),
2580 current_url: url,
2581 origin: Origin::Tuple(
2582 "https".to_string(),
2583 url::Host::Domain("notriddle.com".to_owned()),
2584 443,
2585 ),
2586 redirect_count: 0,
2587 destination: Destination::ServiceWorker,
2588 initiator: Initiator::None,
2589 nonce: String::new(),
2590 integrity_metadata: String::new(),
2591 parser_metadata: ParserMetadata::None,
2592 };
2593
2594 let p = Policy::parse(
2595 "default-src 'none'; script-src 'self' ",
2596 PolicySource::Header,
2597 PolicyDisposition::Enforce,
2598 );
2599
2600 let violation_result = p.does_request_violate_policy(&request);
2601
2602 let expected_result = Violates::Directive(Directive {
2603 name: String::from("script-src"),
2604 value: vec![String::from("'self'")],
2605 });
2606
2607 assert!(violation_result == expected_result);
2608 }
2609
2610 #[test]
2611 pub fn prefetch_request_is_allowed_by_directive() {
2612 let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2613 let request = Request {
2614 url: url.clone(),
2615 current_url: url,
2616 origin: Origin::Tuple(
2617 "https".to_string(),
2618 url::Host::Domain("notriddle.com".to_owned()),
2619 443,
2620 ),
2621 redirect_count: 0,
2622 destination: Destination::Script,
2623 initiator: Initiator::Prefetch,
2624 nonce: String::new(),
2625 integrity_metadata: String::new(),
2626 parser_metadata: ParserMetadata::None,
2627 };
2628
2629 let p = Policy::parse(
2630 "default-src 'none'; child-src 'self'",
2631 PolicySource::Header,
2632 PolicyDisposition::Enforce,
2633 );
2634
2635 let violation_result = p.does_request_violate_policy(&request);
2636
2637 assert!(violation_result == Violates::DoesNotViolate);
2638 }
2639
2640 #[test]
2641 pub fn websocket_request_is_allowed_by_directive() {
2642 let url = Url::parse("https://www.notriddle.com/websocket").unwrap();
2643 let request = Request {
2644 url: url.clone(),
2645 current_url: url,
2646 origin: Origin::Tuple(
2647 "https".to_string(),
2648 url::Host::Domain("notriddle.com".to_owned()),
2649 443,
2650 ),
2651 redirect_count: 0,
2652 destination: Destination::None,
2653 initiator: Initiator::None,
2654 nonce: String::new(),
2655 integrity_metadata: String::new(),
2656 parser_metadata: ParserMetadata::None,
2657 };
2658
2659 let p = Policy::parse(
2660 "connect-src ws://www.notriddle.com/websocket",
2661 PolicySource::Header,
2662 PolicyDisposition::Enforce,
2663 );
2664
2665 let violation_result = p.does_request_violate_policy(&request);
2666
2667 assert!(violation_result == Violates::DoesNotViolate);
2668 }
2669
2670 #[test]
2671 pub fn trusted_type_policy_is_valid() {
2672 let p = Policy::parse(
2673 "trusted-types 'none'",
2674 PolicySource::Meta,
2675 PolicyDisposition::Enforce,
2676 );
2677 assert!(p.is_valid());
2678 assert_eq!(p.directive_set[0].value, vec!["'none'".to_owned()]);
2679 }
2680
2681 #[test]
2682 pub fn non_ascii_character_in_policy_is_invalid() {
2683 let p = Policy::parse(
2684 "trusted-types \u{00A1}'none'",
2685 PolicySource::Meta,
2686 PolicyDisposition::Enforce,
2687 );
2688 assert!(!p.is_valid());
2689 }
2690
2691 #[test]
2692 pub fn csp_list_is_valid() {
2693 let csp_list = CspList::parse(
2694 "default-src 'none'; child-src 'self', trusted-types 'none'",
2695 PolicySource::Meta,
2696 PolicyDisposition::Enforce,
2697 );
2698 assert!(csp_list.is_valid());
2699 assert_eq!(
2700 csp_list.0[1].directive_set[0].value,
2701 vec!["'none'".to_owned()]
2702 );
2703 }
2704
2705 #[test]
2706 pub fn non_ascii_character_in_policy_does_not_effect_other_policy() {
2707 let csp_list = CspList::parse(
2708 "default-src 'none'; child-src \u{00A1}'self', trusted-types 'none'",
2709 PolicySource::Meta,
2710 PolicyDisposition::Enforce,
2711 );
2712 assert!(csp_list.is_valid());
2713 assert_eq!(csp_list.0.len(), 2);
2714 assert_eq!(
2715 csp_list.0[0].directive_set[0].name,
2716 "default-src".to_owned()
2717 );
2718 assert_eq!(
2719 csp_list.0[0].directive_set[0].value,
2720 vec!["'none'".to_owned()]
2721 );
2722 assert_eq!(
2723 csp_list.0[1].directive_set[0].name,
2724 "trusted-types".to_owned()
2725 );
2726 assert_eq!(
2727 csp_list.0[1].directive_set[0].value,
2728 vec!["'none'".to_owned()]
2729 );
2730 }
2731
2732 #[test]
2733 pub fn no_trusted_types_specified_allows_all_policies() {
2734 let csp_list = CspList::parse(
2735 "default-src 'none'; child-src 'self'",
2736 PolicySource::Meta,
2737 PolicyDisposition::Enforce,
2738 );
2739 assert!(csp_list.is_valid());
2740 let (check_result, violations) =
2741 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2742 assert_eq!(check_result, CheckResult::Allowed);
2743 assert!(violations.is_empty());
2744 }
2745
2746 #[test]
2747 pub fn none_does_not_allow_for_any_policy() {
2748 let csp_list = CspList::parse(
2749 "trusted-types 'none'",
2750 PolicySource::Meta,
2751 PolicyDisposition::Enforce,
2752 );
2753 assert!(csp_list.is_valid());
2754 let (check_result, violations) =
2755 csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2756 assert!(check_result == CheckResult::Blocked);
2757 assert_eq!(violations.len(), 1);
2758 }
2759
2760 #[test]
2761 pub fn extra_none_allows_all_policies() {
2762 let csp_list = CspList::parse(
2763 "trusted-types some-policy 'none'",
2764 PolicySource::Meta,
2765 PolicyDisposition::Enforce,
2766 );
2767 assert!(csp_list.is_valid());
2768 let (check_result, violations) =
2769 csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2770 assert!(check_result == CheckResult::Allowed);
2771 assert!(violations.is_empty());
2772 }
2773
2774 #[test]
2775 pub fn explicit_policy_named_is_allowed() {
2776 let csp_list = CspList::parse(
2777 "trusted-types MyPolicy",
2778 PolicySource::Meta,
2779 PolicyDisposition::Enforce,
2780 );
2781 assert!(csp_list.is_valid());
2782 let (check_result, violations) =
2783 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2784 assert_eq!(check_result, CheckResult::Allowed);
2785 assert!(violations.is_empty());
2786 }
2787
2788 #[test]
2789 pub fn other_policy_name_is_blocked() {
2790 let csp_list = CspList::parse(
2791 "trusted-types MyPolicy",
2792 PolicySource::Meta,
2793 PolicyDisposition::Enforce,
2794 );
2795 assert!(csp_list.is_valid());
2796 let (check_result, violations) =
2797 csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2798 assert!(check_result == CheckResult::Blocked);
2799 assert_eq!(violations.len(), 1);
2800 }
2801
2802 #[test]
2803 pub fn invalid_characters_in_policy_name_is_blocked() {
2804 let csp_list = CspList::parse(
2805 "trusted-types My?Policy",
2806 PolicySource::Meta,
2807 PolicyDisposition::Enforce,
2808 );
2809 assert!(csp_list.is_valid());
2810 let (check_result, violations) =
2811 csp_list.is_trusted_type_policy_creation_allowed("My?Policy", &["My?Policy"]);
2812 assert!(check_result == CheckResult::Blocked);
2813 assert_eq!(violations.len(), 1);
2814 }
2815
2816 #[test]
2817 pub fn already_created_policy_is_blocked() {
2818 let csp_list = CspList::parse(
2819 "trusted-types MyPolicy",
2820 PolicySource::Meta,
2821 PolicyDisposition::Enforce,
2822 );
2823 assert!(csp_list.is_valid());
2824 let (check_result, violations) =
2825 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2826 assert!(check_result == CheckResult::Blocked);
2827 assert_eq!(violations.len(), 1);
2828 }
2829
2830 #[test]
2831 pub fn already_created_policy_is_allowed_with_allow_duplicates() {
2832 let csp_list = CspList::parse(
2833 "trusted-types MyPolicy 'allow-duplicates'",
2834 PolicySource::Meta,
2835 PolicyDisposition::Enforce,
2836 );
2837 assert!(csp_list.is_valid());
2838 let (check_result, violations) =
2839 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2840 assert!(check_result == CheckResult::Allowed);
2841 assert!(violations.is_empty());
2842 }
2843
2844 #[test]
2845 pub fn only_report_policy_issues_for_disposition_report() {
2846 let csp_list = CspList::parse(
2847 "trusted-types MyPolicy",
2848 PolicySource::Meta,
2849 PolicyDisposition::Report,
2850 );
2851 assert!(csp_list.is_valid());
2852 let (check_result, violations) =
2853 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2854 assert!(check_result == CheckResult::Allowed);
2855 assert_eq!(violations.len(), 1);
2856 }
2857
2858 #[test]
2859 pub fn wildcard_allows_all_policies() {
2860 let csp_list = CspList::parse(
2861 "trusted-types *",
2862 PolicySource::Meta,
2863 PolicyDisposition::Report,
2864 );
2865 assert!(csp_list.is_valid());
2866 let (check_result, violations) =
2867 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2868 assert!(check_result == CheckResult::Allowed);
2869 assert!(violations.is_empty());
2870 }
2871
2872 #[test]
2873 pub fn violation_has_correct_directive() {
2874 let csp_list = CspList::parse(
2875 "trusted-types MyPolicy",
2876 PolicySource::Meta,
2877 PolicyDisposition::Enforce,
2878 );
2879 assert!(csp_list.is_valid());
2880 let (check_result, violations) =
2881 csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2882 assert!(check_result == CheckResult::Blocked);
2883 assert_eq!(violations.len(), 1);
2884 assert_eq!(violations[0].directive, csp_list.0[0].directive_set[0]);
2885 }
2886
2887 #[test]
2888 pub fn long_policy_name_is_truncated() {
2889 let csp_list = CspList::parse(
2890 "trusted-types MyPolicy",
2891 PolicySource::Meta,
2892 PolicyDisposition::Enforce,
2893 );
2894 assert!(csp_list.is_valid());
2895 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed(
2896 "SuperLongPolicyNameThatExceeds40Characters",
2897 &[],
2898 );
2899 assert!(check_result == CheckResult::Blocked);
2900 assert_eq!(violations.len(), 1);
2901 assert!(
2902 matches!(&violations[0].resource, ViolationResource::TrustedTypePolicy { sample } if sample == "SuperLongPolicyNameThatExceeds40Characte")
2903 );
2904 }
2905}