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 Text,
978 Track,
979 Video,
980 WebIdentity,
981 Worker,
982 Xslt,
983}
984
985pub struct InvalidDestination;
986
987impl FromStr for Destination {
988 type Err = InvalidDestination;
989
990 fn from_str(s: &str) -> Result<Self, Self::Err> {
991 let destination = match s {
992 "" => Self::None,
993 "audio" => Self::Audio,
994 "audioworklet" => Self::AudioWorklet,
995 "document" => Self::Document,
996 "embed" => Self::Embed,
997 "font" => Self::Font,
998 "frame" => Self::Frame,
999 "iframe" => Self::IFrame,
1000 "image" => Self::Image,
1001 "json" => Self::Json,
1002 "manifest" => Self::Manifest,
1003 "object" => Self::Object,
1004 "paintworklet" => Self::PaintWorklet,
1005 "report" => Self::Report,
1006 "script" => Self::Script,
1007 "serviceworker" => Self::ServiceWorker,
1008 "sharedworker" => Self::SharedWorker,
1009 "style" => Self::Style,
1010 "text" => Self::Text,
1011 "track" => Self::Track,
1012 "video" => Self::Video,
1013 "webidentity" => Self::WebIdentity,
1014 "worker" => Self::Worker,
1015 "xslt" => Self::Xslt,
1016 _ => return Err(InvalidDestination),
1017 };
1018
1019 Ok(destination)
1020 }
1021}
1022
1023impl Destination {
1024 pub fn is_script_like(self) -> bool {
1026 use Destination::*;
1027 matches!(
1028 self,
1029 AudioWorklet | PaintWorklet | Script | ServiceWorker | SharedWorker | Worker | Xslt
1030 )
1031 }
1032
1033 pub const fn as_str(&self) -> &'static str {
1034 match self {
1035 Self::None => "",
1036 Self::Audio => "audio",
1037 Self::AudioWorklet => "audioworklet",
1038 Self::Document => "document",
1039 Self::Embed => "embed",
1040 Self::Font => "font",
1041 Self::Frame => "frame",
1042 Self::IFrame => "iframe",
1043 Self::Image => "image",
1044 Self::Json => "json",
1045 Self::Manifest => "manifest",
1046 Self::Object => "object",
1047 Self::PaintWorklet => "paintworklet",
1048 Self::Report => "report",
1049 Self::Script => "script",
1050 Self::ServiceWorker => "serviceworker",
1051 Self::SharedWorker => "sharedworker",
1052 Self::Style => "style",
1053 Self::Text => "text",
1054 Self::Track => "track",
1055 Self::Video => "video",
1056 Self::WebIdentity => "webidentity",
1057 Self::Worker => "worker",
1058 Self::Xslt => "xslt",
1059 }
1060 }
1061}
1062
1063#[derive(Clone, Debug)]
1068pub struct Response {
1069 pub url: Url,
1070 pub redirect_count: u32,
1071}
1072
1073fn is_local_url(url: &Url) -> bool {
1075 let scheme = url.scheme();
1077 scheme == "about" || scheme == "blob" || scheme == "data"
1079}
1080
1081#[derive(Clone, Debug)]
1087#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1088pub struct Violation {
1089 pub resource: ViolationResource,
1090 pub directive: Directive,
1091 pub policy: Policy,
1092}
1093
1094#[derive(Clone, Debug, Eq, PartialEq)]
1100#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1101pub enum ViolationResource {
1102 Url(Url),
1103 Inline { sample: Option<String> },
1104 TrustedTypePolicy { sample: String },
1105 TrustedTypeSink { sample: String },
1106 Eval { sample: Option<String> },
1107 WasmEval,
1108}
1109
1110#[derive(Clone, Debug, Eq, PartialEq)]
1115#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1116pub enum CheckResult {
1117 Allowed,
1118 Blocked,
1119}
1120
1121#[derive(Clone, Debug, Eq, PartialEq)]
1125#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1126pub enum Violates {
1127 DoesNotViolate,
1128 Directive(Directive),
1129}
1130
1131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1133#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1134pub enum PolicyDisposition {
1135 Enforce,
1136 Report,
1137}
1138
1139#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1141#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1142pub enum PolicySource {
1143 Header,
1144 Meta,
1145}
1146
1147#[derive(Clone, Debug, Eq, PartialEq)]
1149#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1150pub struct Directive {
1151 pub name: String,
1152 pub value: Vec<String>,
1153}
1154
1155impl Display for Directive {
1156 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
1157 <str as Display>::fmt(&self.name[..], f)?;
1158 write!(f, " ")?;
1159 for (i, token) in self.value.iter().enumerate() {
1160 if i != 0 {
1161 write!(f, " ")?;
1162 }
1163 <str as Display>::fmt(&token[..], f)?;
1164 }
1165 Ok(())
1166 }
1167}
1168
1169impl Directive {
1170 pub fn is_valid(&self) -> bool {
1172 DIRECTIVE_NAME_GRAMMAR.is_match(&self.name)
1173 && self
1174 .value
1175 .iter()
1176 .all(|t| DIRECTIVE_VALUE_TOKEN_GRAMMAR.is_match(&t[..]))
1177 }
1178 pub fn pre_request_check(&self, request: &Request, policy: &Policy) -> CheckResult {
1180 use CheckResult::*;
1181 match &self.name[..] {
1182 "child-src" => {
1183 let name = get_the_effective_directive_for_request(request);
1184 if !should_fetch_directive_execute(name, "child-src", policy) {
1185 return Allowed;
1186 }
1187 (Directive {
1188 name: String::from(name),
1189 value: self.value.clone(),
1190 })
1191 .pre_request_check(request, policy)
1192 }
1193 "connect-src" => {
1194 let name = get_the_effective_directive_for_request(request);
1195 if !should_fetch_directive_execute(name, "connect-src", policy) {
1196 return Allowed;
1197 }
1198 if SourceList(&self.value[..]).does_request_match_source_list(request)
1199 == DoesNotMatch
1200 {
1201 return Blocked;
1202 }
1203 Allowed
1204 }
1205 "default-src" => {
1206 let name = get_the_effective_directive_for_request(request);
1207 if !should_fetch_directive_execute(name, "default-src", policy) {
1208 return Allowed;
1209 }
1210 (Directive {
1211 name: String::from(name),
1212 value: self.value.clone(),
1213 })
1214 .pre_request_check(request, policy)
1215 }
1216 "font-src" => {
1217 let name = get_the_effective_directive_for_request(request);
1218 if !should_fetch_directive_execute(name, "font-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 "frame-src" => {
1229 let name = get_the_effective_directive_for_request(request);
1230 if !should_fetch_directive_execute(name, "frame-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 "img-src" => {
1241 let name = get_the_effective_directive_for_request(request);
1242 if !should_fetch_directive_execute(name, "img-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 "manifest-src" => {
1253 let name = get_the_effective_directive_for_request(request);
1254 if !should_fetch_directive_execute(name, "manifest-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 "media-src" => {
1265 let name = get_the_effective_directive_for_request(request);
1266 if !should_fetch_directive_execute(name, "media-src", policy) {
1267 return Allowed;
1268 }
1269 if SourceList(&self.value[..]).does_request_match_source_list(request)
1270 == DoesNotMatch
1271 {
1272 return Blocked;
1273 }
1274 Allowed
1275 }
1276 "object-src" => {
1277 let name = get_the_effective_directive_for_request(request);
1278 if !should_fetch_directive_execute(name, "object-src", policy) {
1279 return Allowed;
1280 }
1281 if SourceList(&self.value[..]).does_request_match_source_list(request)
1282 == DoesNotMatch
1283 {
1284 return Blocked;
1285 }
1286 Allowed
1287 }
1288 "script-src" => {
1289 let name = get_the_effective_directive_for_request(request);
1290 if !should_fetch_directive_execute(name, "script-src", policy) {
1291 return Allowed;
1292 }
1293 script_directives_prerequest_check(request, self)
1294 }
1295 "script-src-elem" => {
1296 let name = get_the_effective_directive_for_request(request);
1297 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1298 return Allowed;
1299 }
1300 script_directives_prerequest_check(request, self)
1301 }
1302 "style-src" => {
1303 let name = get_the_effective_directive_for_request(request);
1304 if !should_fetch_directive_execute(name, "style-src", policy) {
1305 return Allowed;
1306 }
1307 let source_list = SourceList(&self.value);
1308 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1309 return Allowed;
1310 }
1311 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1312 return Blocked;
1313 }
1314 Allowed
1315 }
1316 "style-src-elem" => {
1317 let name = get_the_effective_directive_for_request(request);
1318 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1319 return Allowed;
1320 }
1321 let source_list = SourceList(&self.value);
1322 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1323 return Allowed;
1324 }
1325 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1326 return Blocked;
1327 }
1328 Allowed
1329 }
1330 "worker-src" => {
1331 let name = get_the_effective_directive_for_request(request);
1332 if !should_fetch_directive_execute(name, "worker-src", policy) {
1333 return Allowed;
1334 }
1335 let source_list = SourceList(&self.value);
1336 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1337 return Blocked;
1338 }
1339 Allowed
1340 }
1341 _ => Allowed,
1342 }
1343 }
1344 pub fn post_request_check(
1346 &self,
1347 request: &Request,
1348 response: &Response,
1349 policy: &Policy,
1350 ) -> CheckResult {
1351 use CheckResult::*;
1352 match &self.name[..] {
1353 "child-src" => {
1354 let name = get_the_effective_directive_for_request(request);
1355 if !should_fetch_directive_execute(name, "child-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 "connect-src" => {
1365 let name = get_the_effective_directive_for_request(request);
1366 if !should_fetch_directive_execute(name, "connect-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 "default-src" => {
1378 let name = get_the_effective_directive_for_request(request);
1379 if !should_fetch_directive_execute(name, "default-src", policy) {
1380 return Allowed;
1381 }
1382 Directive {
1383 name: name.to_owned(),
1384 value: self.value.clone(),
1385 }
1386 .post_request_check(request, response, policy)
1387 }
1388 "font-src" => {
1389 let name = get_the_effective_directive_for_request(request);
1390 if !should_fetch_directive_execute(name, "font-src", policy) {
1391 return Allowed;
1392 }
1393 let source_list = SourceList(&self.value);
1394 if source_list.does_response_to_request_match_source_list(request, response)
1395 == DoesNotMatch
1396 {
1397 return Blocked;
1398 }
1399 Allowed
1400 }
1401 "frame-src" => {
1402 let name = get_the_effective_directive_for_request(request);
1403 if !should_fetch_directive_execute(name, "frame-src", policy) {
1404 return Allowed;
1405 }
1406 let source_list = SourceList(&self.value);
1407 if source_list.does_response_to_request_match_source_list(request, response)
1408 == DoesNotMatch
1409 {
1410 return Blocked;
1411 }
1412 Allowed
1413 }
1414 "img-src" => {
1415 let name = get_the_effective_directive_for_request(request);
1416 if !should_fetch_directive_execute(name, "img-src", policy) {
1417 return Allowed;
1418 }
1419 let source_list = SourceList(&self.value);
1420 if source_list.does_response_to_request_match_source_list(request, response)
1421 == DoesNotMatch
1422 {
1423 return Blocked;
1424 }
1425 Allowed
1426 }
1427 "manifest-src" => {
1428 let name = get_the_effective_directive_for_request(request);
1429 if !should_fetch_directive_execute(name, "manifest-src", policy) {
1430 return Allowed;
1431 }
1432 let source_list = SourceList(&self.value);
1433 if source_list.does_response_to_request_match_source_list(request, response)
1434 == DoesNotMatch
1435 {
1436 return Blocked;
1437 }
1438 Allowed
1439 }
1440 "media-src" => {
1441 let name = get_the_effective_directive_for_request(request);
1442 if !should_fetch_directive_execute(name, "media-src", policy) {
1443 return Allowed;
1444 }
1445 let source_list = SourceList(&self.value);
1446 if source_list.does_response_to_request_match_source_list(request, response)
1447 == DoesNotMatch
1448 {
1449 return Blocked;
1450 }
1451 Allowed
1452 }
1453 "object-src" => {
1454 let name = get_the_effective_directive_for_request(request);
1455 if !should_fetch_directive_execute(name, "object-src", policy) {
1456 return Allowed;
1457 }
1458 let source_list = SourceList(&self.value);
1459 if source_list.does_response_to_request_match_source_list(request, response)
1460 == DoesNotMatch
1461 {
1462 return Blocked;
1463 }
1464 Allowed
1465 }
1466 "script-src" => {
1467 let name = get_the_effective_directive_for_request(request);
1468 if !should_fetch_directive_execute(name, "script-src", policy) {
1469 return Allowed;
1470 }
1471 script_directives_postrequest_check(request, response, self)
1472 }
1473 "script-src-elem" => {
1474 let name = get_the_effective_directive_for_request(request);
1475 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1476 return Allowed;
1477 }
1478 script_directives_postrequest_check(request, response, self)
1479 }
1480 "style-src" => {
1481 let name = get_the_effective_directive_for_request(request);
1482 if !should_fetch_directive_execute(name, "style-src", policy) {
1483 return Allowed;
1484 }
1485 let source_list = SourceList(&self.value);
1486 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1487 return Allowed;
1488 }
1489 if source_list.does_response_to_request_match_source_list(request, response)
1490 == DoesNotMatch
1491 {
1492 return Blocked;
1493 }
1494 Allowed
1495 }
1496 "style-src-elem" => {
1497 let name = get_the_effective_directive_for_request(request);
1498 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1499 return Allowed;
1500 }
1501 let source_list = SourceList(&self.value);
1502 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1503 return Allowed;
1504 }
1505 if source_list.does_response_to_request_match_source_list(request, response)
1506 == DoesNotMatch
1507 {
1508 return Blocked;
1509 }
1510 Allowed
1511 }
1512 "worker-src" => {
1513 let name = get_the_effective_directive_for_request(request);
1514 if !should_fetch_directive_execute(name, "worker-src", policy) {
1515 return Allowed;
1516 }
1517 let source_list = SourceList(&self.value);
1518 if source_list.does_response_to_request_match_source_list(request, response)
1519 == DoesNotMatch
1520 {
1521 return Blocked;
1522 }
1523 Allowed
1524 }
1525 _ => Allowed,
1526 }
1527 }
1528 pub fn inline_check(
1530 &self,
1531 element: &Element,
1532 type_: InlineCheckType,
1533 policy: &Policy,
1534 source: &str,
1535 ) -> CheckResult {
1536 use CheckResult::*;
1537 match &self.name[..] {
1538 "default-src" => {
1539 let name = get_the_effective_directive_for_inline_checks(type_);
1540 if !should_fetch_directive_execute(name, "default-src", policy) {
1541 return Allowed;
1542 }
1543 Directive {
1544 name: name.to_owned(),
1545 value: self.value.clone(),
1546 }
1547 .inline_check(element, type_, policy, source)
1548 }
1549 "script-src" => {
1550 let name = get_the_effective_directive_for_inline_checks(type_);
1551 if !should_fetch_directive_execute(name, "script-src", policy) {
1552 return Allowed;
1553 }
1554 let source_list = SourceList(&self.value);
1555 if source_list
1556 .does_element_match_source_list_for_type_and_source(element, type_, source)
1557 == DoesNotMatch
1558 {
1559 return Blocked;
1560 }
1561 Allowed
1562 }
1563 "script-src-elem" => {
1564 let name = get_the_effective_directive_for_inline_checks(type_);
1565 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1566 return Allowed;
1567 }
1568 let source_list = SourceList(&self.value);
1569 if source_list
1570 .does_element_match_source_list_for_type_and_source(element, type_, source)
1571 == DoesNotMatch
1572 {
1573 return Blocked;
1574 }
1575 Allowed
1576 }
1577 "script-src-attr" => {
1578 let name = get_the_effective_directive_for_inline_checks(type_);
1579 if !should_fetch_directive_execute(name, "script-src-attr", policy) {
1580 return Allowed;
1581 }
1582 let source_list = SourceList(&self.value);
1583 if source_list
1584 .does_element_match_source_list_for_type_and_source(element, type_, source)
1585 == DoesNotMatch
1586 {
1587 return Blocked;
1588 }
1589 Allowed
1590 }
1591 "style-src" => {
1592 let name = get_the_effective_directive_for_inline_checks(type_);
1593 if !should_fetch_directive_execute(name, "style-src", policy) {
1594 return Allowed;
1595 }
1596 let source_list = SourceList(&self.value);
1597 if source_list
1598 .does_element_match_source_list_for_type_and_source(element, type_, source)
1599 == DoesNotMatch
1600 {
1601 return Blocked;
1602 }
1603 Allowed
1604 }
1605 "style-src-elem" => {
1606 let name = get_the_effective_directive_for_inline_checks(type_);
1607 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1608 return Allowed;
1609 }
1610 let source_list = SourceList(&self.value);
1611 if source_list
1612 .does_element_match_source_list_for_type_and_source(element, type_, source)
1613 == DoesNotMatch
1614 {
1615 return Blocked;
1616 }
1617 Allowed
1618 }
1619 "style-src-attr" => {
1620 let name = get_the_effective_directive_for_inline_checks(type_);
1621 if !should_fetch_directive_execute(name, "style-src-attr", policy) {
1622 return Allowed;
1623 }
1624 let source_list = SourceList(&self.value);
1625 if source_list
1626 .does_element_match_source_list_for_type_and_source(element, type_, source)
1627 == DoesNotMatch
1628 {
1629 return Blocked;
1630 }
1631 Allowed
1632 }
1633 _ => Allowed,
1634 }
1635 }
1636 pub fn get_sandboxing_flag_set_for_document(
1638 &self,
1639 policy: &Policy,
1640 ) -> Option<SandboxingFlagSet> {
1641 debug_assert!(&self.name[..] == "sandbox");
1642 if policy.disposition != PolicyDisposition::Enforce {
1644 None
1645 } else {
1646 Some(parse_a_sandboxing_directive(&self.value[..]))
1648 }
1649 }
1650 pub fn pre_navigation_check<TrustedTypesUrlProcessor>(
1652 &self,
1653 request: &mut Request,
1654 type_: NavigationCheckType,
1655 mut url_processor: TrustedTypesUrlProcessor,
1656 _policy: &Policy,
1657 ) -> CheckResult
1658 where
1659 TrustedTypesUrlProcessor: FnMut(&str) -> Option<String>,
1660 {
1661 use CheckResult::*;
1662 match &self.name[..] {
1663 "form-action" => {
1665 if type_ == NavigationCheckType::FormSubmission {
1667 let source_list = SourceList(&self.value);
1668 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1671 return Blocked;
1672 }
1673 }
1674 Allowed
1676 }
1677 "require-trusted-types-for" => {
1679 let url = &request.url;
1680 if url.scheme() != "javascript" {
1682 return Allowed;
1683 }
1684 let encoded_script_source = &url[Position::AfterScheme..][1..];
1689 let Some(converted_script_source) = url_processor(encoded_script_source) else {
1693 return Blocked;
1694 };
1695 let url_string = "javascript:".to_owned() + &converted_script_source;
1697 let Ok(new_url) = Url::parse(&url_string) else {
1700 return Blocked;
1701 };
1702 request.url = new_url;
1704 Allowed
1706 }
1707 _ => Allowed,
1708 }
1709 }
1710
1711 pub fn navigation_response_check(
1712 &self,
1713 response: &Response,
1714 self_origin: &Origin,
1715 parent_navigable_origins: &Vec<Url>,
1716 _policy: &Policy,
1717 ) -> CheckResult {
1718 use CheckResult::*;
1719 match &self.name[..] {
1720 "frame-ancestors" => {
1722 if is_local_url(&response.url) {
1724 return Allowed;
1725 }
1726 let source_list = SourceList(&self.value);
1735 for origin in parent_navigable_origins {
1739 if source_list.does_url_match_source_list_in_origin_with_redirect_count(
1744 origin,
1745 self_origin,
1746 0,
1747 ) == DoesNotMatch
1748 {
1749 return Blocked;
1750 }
1751 }
1753 Allowed
1755 }
1756 _ => Allowed,
1757 }
1758 }
1759}
1760
1761fn get_the_effective_directive_for_inline_checks(type_: InlineCheckType) -> &'static str {
1763 use InlineCheckType::*;
1764 match type_ {
1765 Script | Navigation => "script-src-elem",
1766 ScriptAttribute => "script-src-attr",
1767 Style => "style-src-elem",
1768 StyleAttribute => "style-src-attr",
1769 }
1770}
1771
1772fn script_directives_prerequest_check(request: &Request, directive: &Directive) -> CheckResult {
1774 use CheckResult::*;
1775 if request_is_script_like(request) {
1777 let source_list = SourceList(&directive.value[..]);
1778 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1781 return Allowed;
1782 }
1783 if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata)
1786 == Matches
1787 {
1788 return Allowed;
1789 }
1790 if directive
1793 .value
1794 .iter()
1795 .any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'"))
1796 {
1797 if request.parser_metadata == ParserMetadata::ParserInserted {
1799 return Blocked;
1800 }
1801 return Allowed;
1803 }
1804
1805 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1808 return Blocked;
1809 }
1810 }
1811 Allowed
1813}
1814
1815fn script_directives_postrequest_check(
1817 request: &Request,
1818 response: &Response,
1819 directive: &Directive,
1820) -> CheckResult {
1821 use CheckResult::*;
1822 if request_is_script_like(request) {
1824 let source_list = SourceList(&directive.value[..]);
1827 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1830 return Allowed;
1831 }
1832 if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata)
1835 == Matches
1836 {
1837 return Allowed;
1838 }
1839 if directive
1841 .value
1842 .iter()
1843 .any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'"))
1844 {
1845 if request.parser_metadata == ParserMetadata::ParserInserted {
1847 return Blocked;
1848 }
1849 return Allowed;
1851 }
1852 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch
1855 {
1856 return Blocked;
1857 }
1858 }
1859 Allowed
1861}
1862
1863fn request_is_script_like(request: &Request) -> bool {
1865 request.destination.is_script_like()
1866}
1867
1868fn should_fetch_directive_execute(
1870 effective_directive_name: &str,
1871 directive_name: &str,
1872 policy: &Policy,
1873) -> bool {
1874 let directive_fallback_list = get_fetch_directive_fallback_list(effective_directive_name);
1875 for fallback_directive in directive_fallback_list {
1876 if directive_name == *fallback_directive {
1877 return true;
1878 }
1879 if policy.contains_a_directive_whose_name_is(fallback_directive) {
1880 return false;
1881 }
1882 }
1883 false
1884}
1885
1886fn get_fetch_directive_fallback_list(directive_name: &str) -> &'static [&'static str] {
1888 match directive_name {
1889 "script-src-elem" => &["script-src-elem", "script-src", "default-src"],
1890 "script-src-attr" => &["script-src-attr", "script-src", "default-src"],
1891 "style-src-elem" => &["style-src-elem", "style-src", "default-src"],
1892 "style-src-attr" => &["style-src-attr", "style-src", "default-src"],
1893 "worker-src" => &["worker-src", "child-src", "script-src", "default-src"],
1894 "connect-src" => &["connect-src", "default-src"],
1895 "manifest-src" => &["manifest-src", "default-src"],
1896 "object-src" => &["object-src", "default-src"],
1897 "frame-src" => &["frame-src", "child-src", "default-src"],
1898 "media-src" => &["media-src", "default-src"],
1899 "font-src" => &["font-src", "default-src"],
1900 "img-src" => &["img-src", "default-src"],
1901 _ => &[],
1902 }
1903}
1904
1905fn get_the_effective_directive_for_request(request: &Request) -> &'static str {
1907 use Destination::*;
1908 use Initiator::*;
1909 if request.initiator == Prefetch || request.initiator == Prerender {
1911 return "default-src";
1912 }
1913 match request.destination {
1915 Destination::Manifest => "manifest-src",
1916 Object | Embed => "object-src",
1917 Frame | IFrame => "frame-src",
1918 Audio | Track | Video => "media-src",
1919 Font => "font-src",
1920 Image => "img-src",
1921 Style => "style-src-elem",
1922 Script | Destination::Xslt | AudioWorklet | PaintWorklet => "script-src-elem",
1923 ServiceWorker | SharedWorker | Worker => "worker-src",
1924 Json | Text | WebIdentity => "connect-src",
1925 Report => "",
1926 _ => "connect-src",
1928 }
1929}
1930
1931#[derive(Clone, Debug, Eq, PartialEq)]
1933pub enum MatchResult {
1934 Matches,
1935 DoesNotMatch,
1936}
1937
1938static DIRECTIVE_NAME_GRAMMAR: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^[0-9a-z\-]+$"#).unwrap());
1940static DIRECTIVE_VALUE_TOKEN_GRAMMAR: Lazy<Regex> =
1942 Lazy::new(|| Regex::new(r#"^[\u{21}-\u{2B}\u{2D}-\u{3A}\u{3C}-\u{7E}]+$"#).unwrap());
1943static NONCE_SOURCE_GRAMMAR: Lazy<Regex> =
1945 Lazy::new(|| Regex::new(r#"^'nonce-(?P<n>[a-zA-Z0-9\+/\-_]+=*)'$"#).unwrap());
1946static NONE_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^'none'$"#).unwrap());
1947static SCHEME_SOURCE_GRAMMAR: Lazy<Regex> =
1949 Lazy::new(|| Regex::new(r#"^(?P<scheme>[a-zA-Z][a-zA-Z0-9\+\-\.]*):$"#).unwrap());
1950static HOST_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
1952 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()
1954});
1955static HASH_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
1957 Regex::new(r#"^'(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)'$"#)
1958 .unwrap()
1959});
1960
1961#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1963struct SourceList<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item = &'a U>>(I);
1964
1965impl<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item = &'a U>> SourceList<'a, U, I> {
1966 fn does_nonce_match_source_list(&self, nonce: &str) -> MatchResult {
1968 if nonce.is_empty() {
1969 return DoesNotMatch;
1970 };
1971 for expression in self.0.clone().into_iter() {
1972 if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression.borrow()) {
1973 if let Some(captured_nonce) = captures.name("n") {
1974 if nonce == captured_nonce.as_str() {
1975 return Matches;
1976 }
1977 }
1978 }
1979 }
1980 DoesNotMatch
1981 }
1982 fn does_integrity_metadata_match_source_list(&self, integrity_metadata: &str) -> MatchResult {
1984 let integrity_expressions: Vec<HashFunction> = self
1986 .0
1987 .clone()
1988 .into_iter()
1989 .filter_map(|expression| {
1990 if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression.borrow()) {
1991 if let (Some(algorithm), Some(value)) = (
1992 captures
1993 .name("algorithm")
1994 .and_then(|a| HashAlgorithm::from_name(a.as_str())),
1995 captures.name("value"),
1996 ) {
1997 return Some(HashFunction {
1998 algorithm,
1999 value: String::from(value.as_str()),
2000 });
2001 }
2002 }
2003 None
2004 })
2005 .collect();
2006 if integrity_expressions.is_empty() {
2008 return DoesNotMatch;
2009 }
2010 let integrity_sources = parse_subresource_integrity_metadata(integrity_metadata);
2012 match integrity_sources {
2013 SubresourceIntegrityMetadata::NoMetadata => DoesNotMatch,
2015 SubresourceIntegrityMetadata::IntegritySources(integrity_sources) => {
2016 if integrity_sources.is_empty() {
2017 return DoesNotMatch;
2018 }
2019 for source in &integrity_sources {
2021 if !integrity_expressions.iter().any(|ex| ex == source) {
2028 return DoesNotMatch;
2029 }
2030 }
2031 Matches
2033 }
2034 }
2035 }
2036 fn does_request_match_source_list(&self, request: &Request) -> MatchResult {
2038 self.does_url_match_source_list_in_origin_with_redirect_count(
2043 &request.current_url,
2044 &request.origin,
2045 request.redirect_count,
2046 )
2047 }
2048 fn does_url_match_source_list_in_origin_with_redirect_count(
2050 &self,
2051 url: &Url,
2052 origin: &Origin,
2053 redirect_count: u32,
2054 ) -> MatchResult {
2055 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2056 if NONE_SOURCE_GRAMMAR.is_match(expression) {
2057 continue;
2058 };
2059 let result = does_url_match_expression_in_origin_with_redirect_count(
2060 url,
2061 expression,
2062 origin,
2063 redirect_count,
2064 );
2065 if result == Matches {
2066 return Matches;
2067 }
2068 }
2069 DoesNotMatch
2070 }
2071 fn does_element_match_source_list_for_type_and_source(
2073 &self,
2074 element: &Element,
2075 type_: InlineCheckType,
2076 source: &str,
2077 ) -> MatchResult {
2078 if self.does_a_source_list_allow_all_inline_behavior_for_type(type_) == AllowResult::Allows
2079 {
2080 return Matches;
2081 }
2082 if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style {
2083 if let Some(nonce) = element.nonce.as_ref() {
2084 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2085 if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression) {
2086 if let Some(captured_nonce) = captures.name("n") {
2087 if nonce == captured_nonce.as_str() {
2088 return Matches;
2089 }
2090 }
2091 }
2092 }
2093 }
2094 }
2095 let mut unsafe_hashes = false;
2096 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2097 if ascii_case_insensitive_match(expression, "'unsafe-hashes'") {
2098 unsafe_hashes = true;
2099 break;
2100 }
2101 }
2102 if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style || unsafe_hashes {
2103 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2104 if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression) {
2105 if let (Some(algorithm), Some(value)) = (
2106 captures
2107 .name("algorithm")
2108 .and_then(|a| HashAlgorithm::from_name(a.as_str())),
2109 captures.name("value"),
2110 ) {
2111 let actual = algorithm.apply(source);
2112 let expected = value.as_str().replace('-', "+").replace('_', "/");
2113 if actual == expected {
2114 return Matches;
2115 }
2116 }
2117 }
2118 }
2119 }
2120 DoesNotMatch
2121 }
2122 fn does_a_source_list_allow_all_inline_behavior_for_type(
2124 &self,
2125 type_: InlineCheckType,
2126 ) -> AllowResult {
2127 use InlineCheckType::*;
2128 let mut allow_all_inline = false;
2129 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2130 if HASH_SOURCE_GRAMMAR.is_match(expression) || NONCE_SOURCE_GRAMMAR.is_match(expression)
2131 {
2132 return AllowResult::DoesNotAllow;
2133 }
2134 if (type_ == Script || type_ == ScriptAttribute || type_ == Navigation)
2135 && expression == "'strict-dynamic'"
2136 {
2137 return AllowResult::DoesNotAllow;
2138 }
2139 if ascii_case_insensitive_match(expression, "'unsafe-inline'") {
2140 allow_all_inline = true;
2141 }
2142 }
2143 if allow_all_inline {
2144 AllowResult::Allows
2145 } else {
2146 AllowResult::DoesNotAllow
2147 }
2148 }
2149 fn does_response_to_request_match_source_list(
2151 &self,
2152 request: &Request,
2153 response: &Response,
2154 ) -> MatchResult {
2155 self.does_url_match_source_list_in_origin_with_redirect_count(
2156 &response.url,
2157 &request.origin,
2158 response.redirect_count,
2159 )
2160 }
2161 fn does_a_source_list_allow_js_evaluation(&self) -> AllowResult {
2163 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2164 if ascii_case_insensitive_match(expression, "'unsafe-eval'") {
2167 return AllowResult::Allows;
2168 }
2169 }
2170 AllowResult::DoesNotAllow
2171 }
2172 fn does_a_source_list_allow_wasm_evaluation(&self) -> AllowResult {
2174 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2175 if ascii_case_insensitive_match(expression, "'unsafe-eval'")
2176 || ascii_case_insensitive_match(expression, "'wasm-unsafe-eval'")
2177 {
2178 return AllowResult::Allows;
2179 }
2180 }
2181 AllowResult::DoesNotAllow
2182 }
2183}
2184
2185#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2187enum AllowResult {
2188 Allows,
2189 DoesNotAllow,
2190}
2191
2192fn does_url_match_expression_in_origin_with_redirect_count(
2194 url: &Url,
2195 expression: &str,
2196 origin: &Origin,
2197 redirect_count: u32,
2198) -> MatchResult {
2199 let url_scheme = url.scheme();
2201 if expression == "*" {
2202 if scheme_is_network(url_scheme) {
2204 return Matches;
2205 }
2206 return origin_scheme_part_match(origin, url_scheme);
2208 }
2209 if let Some(captures) = SCHEME_SOURCE_GRAMMAR.captures(expression) {
2211 if let Some(expression_scheme) = captures.name("scheme") {
2215 return scheme_part_match(expression_scheme.as_str(), url_scheme);
2216 }
2217 return DoesNotMatch;
2219 }
2220 if let Some(captures) = HOST_SOURCE_GRAMMAR.captures(expression) {
2222 let expr_has_scheme_part = if let Some(expression_scheme) = captures.name("scheme") {
2223 if scheme_part_match(expression_scheme.as_str(), url_scheme) != Matches {
2224 return DoesNotMatch;
2225 }
2226 true
2227 } else {
2228 false
2229 };
2230 let url_host = if let Some(url_host) = url.host() {
2231 url_host
2232 } else {
2233 return DoesNotMatch;
2235 };
2236 if !expr_has_scheme_part && origin_scheme_part_match(origin, url.scheme()) != Matches {
2240 return DoesNotMatch;
2241 }
2242 if let Some(expression_host) = captures.name("host") {
2244 if host_part_match(expression_host.as_str(), &url_host.to_string()) != Matches {
2245 return DoesNotMatch;
2246 }
2247 } else {
2248 return DoesNotMatch;
2250 }
2251 let port_part = captures.name("port").map(|port| &port.as_str()[1..]);
2255 if port_part_match(port_part, url) != Matches {
2257 return DoesNotMatch;
2258 }
2259 let path_part = captures
2261 .name("path")
2262 .map(|path_part| path_part.as_str())
2263 .unwrap_or("");
2264 if path_part != "/" && redirect_count == 0 {
2265 let path = url.path();
2267 if path_part_match(path_part, path) != Matches {
2269 return DoesNotMatch;
2270 }
2271 }
2272 return Matches;
2274 }
2275 if ascii_case_insensitive_match(expression, "'self'") {
2277 if url.scheme() == "blob" {
2279 return DoesNotMatch;
2280 }
2281 if *origin == url.origin() {
2284 return Matches;
2285 }
2286 if let Origin::Tuple(scheme, host, port) = origin {
2290 let hosts_are_the_same = Some(host) == url.host().map(|p| p.to_owned()).as_ref();
2291 let ports_are_the_same = Some(*port) == url.port();
2292 let origins_port_is_default_for_scheme = Some(*port) == default_port(scheme);
2293 let url_port_is_default_port_for_scheme =
2294 url.port() == default_port(scheme) && default_port(scheme).is_some();
2295 let ports_are_default =
2296 url_port_is_default_port_for_scheme && origins_port_is_default_for_scheme;
2297 if hosts_are_the_same
2298 && (ports_are_the_same || ports_are_default)
2299 && ((url_scheme == "https" || url_scheme == "wss")
2301 || (scheme == "http" && (url_scheme == "http" || url_scheme == "ws")))
2303 {
2304 return Matches;
2305 }
2306 }
2307 }
2308 DoesNotMatch
2310}
2311
2312fn host_part_match(pattern: &str, host: &str) -> MatchResult {
2314 debug_assert!(!host.is_empty());
2315 if host.is_empty() {
2317 return DoesNotMatch;
2318 }
2319 if pattern.as_bytes()[0] == b'*' {
2320 if pattern.len() == 1 {
2322 return Matches;
2323 }
2324 if pattern.as_bytes()[1] == b'.' {
2326 let remaining_pattern = &pattern[1..];
2328 if remaining_pattern.len() > host.len() {
2329 return DoesNotMatch;
2330 }
2331 let remaining_host = &host[(host.len() - remaining_pattern.len())..];
2332 debug_assert_eq!(remaining_host.len(), remaining_pattern.len());
2333 if ascii_case_insensitive_match(remaining_pattern, remaining_host) {
2335 return Matches;
2336 }
2337 return DoesNotMatch;
2339 }
2340 }
2341 if !ascii_case_insensitive_match(pattern, host) {
2343 return DoesNotMatch;
2344 }
2345 static IPV4_ADDRESS_RULE: Lazy<Regex> = Lazy::new(|| {
2346 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()
2347 });
2348 if IPV4_ADDRESS_RULE.is_match(pattern) && pattern != "127.0.0.1" {
2349 return DoesNotMatch;
2350 }
2351 if pattern.as_bytes()[0] == b'[' {
2355 return DoesNotMatch;
2356 }
2357 Matches
2359}
2360
2361fn port_part_match(input: Option<&str>, url: &Url) -> MatchResult {
2363 use std::str::FromStr;
2364 debug_assert!(input.is_none() || input == Some("*") || u16::from_str(input.unwrap()).is_ok());
2366 if input == Some("*") {
2368 return Matches;
2369 }
2370 let normalized_input = if let Some(input) = input {
2372 u16::from_str(&input).ok()
2373 } else {
2374 None
2375 };
2376 if normalized_input == url.port() {
2378 return Matches;
2379 }
2380 if url.port().is_none() {
2382 let default_port = default_port(url.scheme());
2384 if normalized_input == default_port {
2386 return Matches;
2387 }
2388 }
2389 DoesNotMatch
2391}
2392
2393fn path_part_match(path_a: &str, path_b: &str) -> MatchResult {
2395 if path_a.is_empty() {
2396 return Matches;
2397 }
2398 if path_a == "/" && path_b.is_empty() {
2399 return Matches;
2400 }
2401 let exact_match = path_a.as_bytes()[path_a.len() - 1] != b'/';
2402 let (mut path_list_a, path_list_b): (Vec<&str>, Vec<&str>) =
2403 (path_a.split('/').collect(), path_b.split('/').collect());
2404 if path_list_a.len() > path_list_b.len() {
2405 return DoesNotMatch;
2406 }
2407 if exact_match && path_list_a.len() != path_list_b.len() {
2408 return DoesNotMatch;
2409 }
2410 if !exact_match {
2411 debug_assert_eq!(path_list_a[path_list_a.len() - 1], "");
2412 path_list_a.pop();
2413 }
2414 let mut piece_b_iter = path_list_b.iter();
2415 for piece_a in &path_list_a {
2416 let piece_b = piece_b_iter.next().unwrap();
2417 let piece_a: Vec<u8> = percent_encoding::percent_decode(piece_a.as_bytes()).collect();
2418 let piece_b: Vec<u8> = percent_encoding::percent_decode(piece_b.as_bytes()).collect();
2419 if piece_a != piece_b {
2420 return DoesNotMatch;
2421 }
2422 }
2423 Matches
2424}
2425
2426fn default_port(scheme: &str) -> Option<u16> {
2427 Some(match scheme {
2428 "ftp" => 21,
2429 "gopher" => 70,
2430 "http" => 80,
2431 "https" => 443,
2432 "ws" => 80,
2433 "wss" => 443,
2434 _ => return None,
2435 })
2436}
2437
2438fn origin_scheme_part_match(a: &Origin, b: &str) -> MatchResult {
2439 if let Origin::Tuple(scheme, _host, _port) = a {
2440 scheme_part_match(&scheme[..], b)
2441 } else {
2442 DoesNotMatch
2443 }
2444}
2445
2446fn scheme_part_match(a: &str, b: &str) -> MatchResult {
2448 let a = a.to_ascii_lowercase();
2449 let b = b.to_ascii_lowercase();
2450 match (&a[..], &b[..]) {
2451 _ if a == b => Matches,
2452 ("http", "https") | ("ws", "wss" | "http" | "https") | ("wss", "https") => Matches,
2453 _ => DoesNotMatch,
2454 }
2455}
2456
2457#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2458pub enum HashAlgorithm {
2459 Sha256,
2460 Sha384,
2461 Sha512,
2462}
2463
2464impl HashAlgorithm {
2465 pub fn from_name(name: &str) -> Option<HashAlgorithm> {
2466 use HashAlgorithm::*;
2467 match name {
2468 "sha256" | "Sha256" | "sHa256" | "shA256" | "SHa256" | "ShA256" | "sHA256"
2469 | "SHA256" => Some(Sha256),
2470 "sha384" | "Sha384" | "sHa384" | "shA384" | "SHa384" | "ShA384" | "sHA384"
2471 | "SHA384" => Some(Sha384),
2472 "sha512" | "Sha512" | "sHa512" | "shA512" | "SHa512" | "ShA512" | "sHA512"
2473 | "SHA512" => Some(Sha512),
2474 _ => None,
2475 }
2476 }
2477 pub fn apply(self, value: &str) -> String {
2478 use base64::Engine as _;
2479 let bytes = value.as_bytes();
2480 let standard = base64::engine::general_purpose::STANDARD;
2481 match self {
2482 HashAlgorithm::Sha256 => standard.encode(sha2::Sha256::digest(bytes)),
2483 HashAlgorithm::Sha384 => standard.encode(sha2::Sha384::digest(bytes)),
2484 HashAlgorithm::Sha512 => standard.encode(sha2::Sha512::digest(bytes)),
2485 }
2486 }
2487}
2488
2489#[derive(Clone, Debug, Eq, PartialEq)]
2491pub struct HashFunction {
2492 algorithm: HashAlgorithm,
2493 value: String,
2494 }
2496
2497#[derive(Clone, Debug, Eq, PartialEq)]
2499pub enum SubresourceIntegrityMetadata {
2500 NoMetadata,
2501 IntegritySources(Vec<HashFunction>),
2502}
2503
2504static SUBRESOURCE_METADATA_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
2507 Regex::new(r#"(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)"#)
2508 .unwrap()
2509});
2510
2511pub fn parse_subresource_integrity_metadata(string: &str) -> SubresourceIntegrityMetadata {
2513 let mut result = Vec::new();
2514 let mut empty = true;
2515 for token in split_ascii_whitespace(string) {
2516 empty = false;
2517 if let Some(captures) = SUBRESOURCE_METADATA_GRAMMAR.captures(token) {
2518 if let (Some(algorithm), Some(value)) = (
2519 captures
2520 .name("algorithm")
2521 .and_then(|a| HashAlgorithm::from_name(a.as_str())),
2522 captures.name("value"),
2523 ) {
2524 result.push(HashFunction {
2525 algorithm,
2526 value: String::from(value.as_str()),
2527 });
2528 }
2529 }
2530 }
2531 if empty {
2532 SubresourceIntegrityMetadata::NoMetadata
2533 } else {
2534 SubresourceIntegrityMetadata::IntegritySources(result)
2535 }
2536}
2537
2538#[cfg(test)]
2539mod test {
2540 use super::*;
2541 #[test]
2542 fn empty_directive_is_not_valid() {
2543 let d = Directive {
2544 name: String::new(),
2545 value: Vec::new(),
2546 };
2547 assert!(!d.is_valid());
2548 }
2549 #[test]
2550 pub fn duplicate_policy_is_not_valid() {
2551 let d = Directive {
2552 name: "test".to_owned(),
2553 value: vec!["test".to_owned()],
2554 };
2555 let p = Policy {
2556 directive_set: vec![d.clone(), d.clone()],
2557 disposition: PolicyDisposition::Enforce,
2558 source: PolicySource::Header,
2559 };
2560 assert!(!p.is_valid());
2561 }
2562 #[test]
2563 pub fn basic_policy_is_valid() {
2564 let p = Policy::parse(
2565 "script-src notriddle.com",
2566 PolicySource::Header,
2567 PolicyDisposition::Enforce,
2568 );
2569 assert!(p.is_valid());
2570 }
2571 #[test]
2572 pub fn policy_with_empty_directive_set_is_not_valid() {
2573 let p = Policy {
2574 directive_set: vec![],
2575 disposition: PolicyDisposition::Enforce,
2576 source: PolicySource::Header,
2577 };
2578 assert!(!p.is_valid());
2579 }
2580
2581 #[test]
2582 pub fn prefetch_request_does_not_violate_policy() {
2583 let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2584 let request = Request {
2585 url: url.clone(),
2586 current_url: url,
2587 origin: Origin::Tuple(
2588 "https".to_string(),
2589 url::Host::Domain("notriddle.com".to_owned()),
2590 443,
2591 ),
2592 redirect_count: 0,
2593 destination: Destination::Script,
2594 initiator: Initiator::Prefetch,
2595 nonce: String::new(),
2596 integrity_metadata: String::new(),
2597 parser_metadata: ParserMetadata::None,
2598 };
2599
2600 let p = Policy::parse(
2601 "child-src 'self'",
2602 PolicySource::Header,
2603 PolicyDisposition::Enforce,
2604 );
2605
2606 let violation_result = p.does_request_violate_policy(&request);
2607
2608 assert!(violation_result == Violates::DoesNotViolate);
2609 }
2610
2611 #[test]
2612 pub fn prefetch_request_violates_policy() {
2613 let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2614 let request = Request {
2615 url: url.clone(),
2616 current_url: url,
2617 origin: Origin::Tuple(
2618 "https".to_string(),
2619 url::Host::Domain("notriddle.com".to_owned()),
2620 443,
2621 ),
2622 redirect_count: 0,
2623 destination: Destination::ServiceWorker,
2624 initiator: Initiator::None,
2625 nonce: String::new(),
2626 integrity_metadata: String::new(),
2627 parser_metadata: ParserMetadata::None,
2628 };
2629
2630 let p = Policy::parse(
2631 "default-src 'none'; script-src 'self' ",
2632 PolicySource::Header,
2633 PolicyDisposition::Enforce,
2634 );
2635
2636 let violation_result = p.does_request_violate_policy(&request);
2637
2638 let expected_result = Violates::Directive(Directive {
2639 name: String::from("script-src"),
2640 value: vec![String::from("'self'")],
2641 });
2642
2643 assert!(violation_result == expected_result);
2644 }
2645
2646 #[test]
2647 pub fn prefetch_request_is_allowed_by_directive() {
2648 let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2649 let request = Request {
2650 url: url.clone(),
2651 current_url: url,
2652 origin: Origin::Tuple(
2653 "https".to_string(),
2654 url::Host::Domain("notriddle.com".to_owned()),
2655 443,
2656 ),
2657 redirect_count: 0,
2658 destination: Destination::Script,
2659 initiator: Initiator::Prefetch,
2660 nonce: String::new(),
2661 integrity_metadata: String::new(),
2662 parser_metadata: ParserMetadata::None,
2663 };
2664
2665 let p = Policy::parse(
2666 "default-src 'none'; child-src 'self'",
2667 PolicySource::Header,
2668 PolicyDisposition::Enforce,
2669 );
2670
2671 let violation_result = p.does_request_violate_policy(&request);
2672
2673 assert!(violation_result == Violates::DoesNotViolate);
2674 }
2675
2676 #[test]
2677 pub fn blob_url_does_not_match_self() {
2678 let url = Url::parse("blob:https://www.notriddle.com/script.js").unwrap();
2679 let request = Request {
2680 url: url.clone(),
2681 current_url: url,
2682 origin: Origin::Tuple(
2683 "https".to_string(),
2684 url::Host::Domain("notriddle.com".to_owned()),
2685 443,
2686 ),
2687 redirect_count: 0,
2688 destination: Destination::Worker,
2689 initiator: Initiator::None,
2690 nonce: String::new(),
2691 integrity_metadata: String::new(),
2692 parser_metadata: ParserMetadata::None,
2693 };
2694
2695 let p = Policy::parse(
2696 "worker-src 'self'",
2697 PolicySource::Header,
2698 PolicyDisposition::Enforce,
2699 );
2700
2701 let violation_result = p.does_request_violate_policy(&request);
2702
2703 let expected_result = Violates::Directive(Directive {
2704 name: String::from("worker-src"),
2705 value: vec![String::from("'self'")],
2706 });
2707
2708 assert!(violation_result == expected_result);
2709 }
2710
2711 #[test]
2712 pub fn websocket_request_is_allowed_by_directive() {
2713 let url = Url::parse("https://www.notriddle.com/websocket").unwrap();
2714 let request = Request {
2715 url: url.clone(),
2716 current_url: url,
2717 origin: Origin::Tuple(
2718 "https".to_string(),
2719 url::Host::Domain("notriddle.com".to_owned()),
2720 443,
2721 ),
2722 redirect_count: 0,
2723 destination: Destination::None,
2724 initiator: Initiator::None,
2725 nonce: String::new(),
2726 integrity_metadata: String::new(),
2727 parser_metadata: ParserMetadata::None,
2728 };
2729
2730 let p = Policy::parse(
2731 "connect-src ws://www.notriddle.com/websocket",
2732 PolicySource::Header,
2733 PolicyDisposition::Enforce,
2734 );
2735
2736 let violation_result = p.does_request_violate_policy(&request);
2737
2738 assert!(violation_result == Violates::DoesNotViolate);
2739 }
2740
2741 #[test]
2742 pub fn trusted_type_policy_is_valid() {
2743 let p = Policy::parse(
2744 "trusted-types 'none'",
2745 PolicySource::Meta,
2746 PolicyDisposition::Enforce,
2747 );
2748 assert!(p.is_valid());
2749 assert_eq!(p.directive_set[0].value, vec!["'none'".to_owned()]);
2750 }
2751
2752 #[test]
2753 pub fn non_ascii_character_in_policy_is_invalid() {
2754 let p = Policy::parse(
2755 "trusted-types \u{00A1}'none'",
2756 PolicySource::Meta,
2757 PolicyDisposition::Enforce,
2758 );
2759 assert!(!p.is_valid());
2760 }
2761
2762 #[test]
2763 pub fn csp_list_is_valid() {
2764 let csp_list = CspList::parse(
2765 "default-src 'none'; child-src 'self', trusted-types 'none'",
2766 PolicySource::Meta,
2767 PolicyDisposition::Enforce,
2768 );
2769 assert!(csp_list.is_valid());
2770 assert_eq!(
2771 csp_list.0[1].directive_set[0].value,
2772 vec!["'none'".to_owned()]
2773 );
2774 }
2775
2776 #[test]
2777 pub fn non_ascii_character_in_policy_does_not_effect_other_policy() {
2778 let csp_list = CspList::parse(
2779 "default-src 'none'; child-src \u{00A1}'self', trusted-types 'none'",
2780 PolicySource::Meta,
2781 PolicyDisposition::Enforce,
2782 );
2783 assert!(csp_list.is_valid());
2784 assert_eq!(csp_list.0.len(), 2);
2785 assert_eq!(
2786 csp_list.0[0].directive_set[0].name,
2787 "default-src".to_owned()
2788 );
2789 assert_eq!(
2790 csp_list.0[0].directive_set[0].value,
2791 vec!["'none'".to_owned()]
2792 );
2793 assert_eq!(
2794 csp_list.0[1].directive_set[0].name,
2795 "trusted-types".to_owned()
2796 );
2797 assert_eq!(
2798 csp_list.0[1].directive_set[0].value,
2799 vec!["'none'".to_owned()]
2800 );
2801 }
2802
2803 #[test]
2804 pub fn no_trusted_types_specified_allows_all_policies() {
2805 let csp_list = CspList::parse(
2806 "default-src 'none'; child-src 'self'",
2807 PolicySource::Meta,
2808 PolicyDisposition::Enforce,
2809 );
2810 assert!(csp_list.is_valid());
2811 let (check_result, violations) =
2812 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2813 assert_eq!(check_result, CheckResult::Allowed);
2814 assert!(violations.is_empty());
2815 }
2816
2817 #[test]
2818 pub fn none_does_not_allow_for_any_policy() {
2819 let csp_list = CspList::parse(
2820 "trusted-types 'none'",
2821 PolicySource::Meta,
2822 PolicyDisposition::Enforce,
2823 );
2824 assert!(csp_list.is_valid());
2825 let (check_result, violations) =
2826 csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2827 assert!(check_result == CheckResult::Blocked);
2828 assert_eq!(violations.len(), 1);
2829 }
2830
2831 #[test]
2832 pub fn extra_none_allows_all_policies() {
2833 let csp_list = CspList::parse(
2834 "trusted-types some-policy 'none'",
2835 PolicySource::Meta,
2836 PolicyDisposition::Enforce,
2837 );
2838 assert!(csp_list.is_valid());
2839 let (check_result, violations) =
2840 csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2841 assert!(check_result == CheckResult::Allowed);
2842 assert!(violations.is_empty());
2843 }
2844
2845 #[test]
2846 pub fn explicit_policy_named_is_allowed() {
2847 let csp_list = CspList::parse(
2848 "trusted-types MyPolicy",
2849 PolicySource::Meta,
2850 PolicyDisposition::Enforce,
2851 );
2852 assert!(csp_list.is_valid());
2853 let (check_result, violations) =
2854 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2855 assert_eq!(check_result, CheckResult::Allowed);
2856 assert!(violations.is_empty());
2857 }
2858
2859 #[test]
2860 pub fn other_policy_name_is_blocked() {
2861 let csp_list = CspList::parse(
2862 "trusted-types MyPolicy",
2863 PolicySource::Meta,
2864 PolicyDisposition::Enforce,
2865 );
2866 assert!(csp_list.is_valid());
2867 let (check_result, violations) =
2868 csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2869 assert!(check_result == CheckResult::Blocked);
2870 assert_eq!(violations.len(), 1);
2871 }
2872
2873 #[test]
2874 pub fn invalid_characters_in_policy_name_is_blocked() {
2875 let csp_list = CspList::parse(
2876 "trusted-types My?Policy",
2877 PolicySource::Meta,
2878 PolicyDisposition::Enforce,
2879 );
2880 assert!(csp_list.is_valid());
2881 let (check_result, violations) =
2882 csp_list.is_trusted_type_policy_creation_allowed("My?Policy", &["My?Policy"]);
2883 assert!(check_result == CheckResult::Blocked);
2884 assert_eq!(violations.len(), 1);
2885 }
2886
2887 #[test]
2888 pub fn already_created_policy_is_blocked() {
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) =
2896 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2897 assert!(check_result == CheckResult::Blocked);
2898 assert_eq!(violations.len(), 1);
2899 }
2900
2901 #[test]
2902 pub fn already_created_policy_is_allowed_with_allow_duplicates() {
2903 let csp_list = CspList::parse(
2904 "trusted-types MyPolicy 'allow-duplicates'",
2905 PolicySource::Meta,
2906 PolicyDisposition::Enforce,
2907 );
2908 assert!(csp_list.is_valid());
2909 let (check_result, violations) =
2910 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2911 assert!(check_result == CheckResult::Allowed);
2912 assert!(violations.is_empty());
2913 }
2914
2915 #[test]
2916 pub fn only_report_policy_issues_for_disposition_report() {
2917 let csp_list = CspList::parse(
2918 "trusted-types MyPolicy",
2919 PolicySource::Meta,
2920 PolicyDisposition::Report,
2921 );
2922 assert!(csp_list.is_valid());
2923 let (check_result, violations) =
2924 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2925 assert!(check_result == CheckResult::Allowed);
2926 assert_eq!(violations.len(), 1);
2927 }
2928
2929 #[test]
2930 pub fn wildcard_allows_all_policies() {
2931 let csp_list = CspList::parse(
2932 "trusted-types *",
2933 PolicySource::Meta,
2934 PolicyDisposition::Report,
2935 );
2936 assert!(csp_list.is_valid());
2937 let (check_result, violations) =
2938 csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2939 assert!(check_result == CheckResult::Allowed);
2940 assert!(violations.is_empty());
2941 }
2942
2943 #[test]
2944 pub fn violation_has_correct_directive() {
2945 let csp_list = CspList::parse(
2946 "trusted-types MyPolicy",
2947 PolicySource::Meta,
2948 PolicyDisposition::Enforce,
2949 );
2950 assert!(csp_list.is_valid());
2951 let (check_result, violations) =
2952 csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2953 assert!(check_result == CheckResult::Blocked);
2954 assert_eq!(violations.len(), 1);
2955 assert_eq!(violations[0].directive, csp_list.0[0].directive_set[0]);
2956 }
2957
2958 #[test]
2959 pub fn long_policy_name_is_truncated() {
2960 let csp_list = CspList::parse(
2961 "trusted-types MyPolicy",
2962 PolicySource::Meta,
2963 PolicyDisposition::Enforce,
2964 );
2965 assert!(csp_list.is_valid());
2966 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed(
2967 "SuperLongPolicyNameThatExceeds40Characters",
2968 &[],
2969 );
2970 assert!(check_result == CheckResult::Blocked);
2971 assert_eq!(violations.len(), 1);
2972 assert!(
2973 matches!(&violations[0].resource, ViolationResource::TrustedTypePolicy { sample } if sample == "SuperLongPolicyNameThatExceeds40Characte")
2974 );
2975 }
2976}