1#![forbid(unsafe_code)]
38
39pub extern crate url;
40pub extern crate percent_encoding;
41
42pub(crate) mod text_util;
43pub mod sandboxing_directive;
44
45pub use url::{Origin, Position, Url};
46#[cfg(feature = "serde")] use serde::{Deserialize, Serialize};
47use sha2::Digest;
48use std::borrow::{Borrow, Cow};
49use std::cmp;
50use std::fmt::{self, Display, Formatter};
51use std::str::FromStr;
52use text_util::{
53 strip_leading_and_trailing_ascii_whitespace,
54 split_ascii_whitespace,
55 split_commas,
56 ascii_case_insensitive_match,
57 collect_a_sequence_of_non_ascii_white_space_code_points,
58};
59use sandboxing_directive::{SandboxingFlagSet, parse_a_sandboxing_directive};
60use MatchResult::Matches;
61use MatchResult::DoesNotMatch;
62use std::collections::HashSet;
63use regex::Regex;
64use once_cell::sync::Lazy;
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.directive_set.iter().map(|d| d.name.clone()).collect::<HashSet<_>>().len() == self.directive_set.len() &&
103 !self.directive_set.is_empty()
104 }
105 pub fn parse(serialized: &str, source: PolicySource, disposition: PolicyDisposition) -> Policy {
107 let mut policy = Policy {
108 directive_set: Vec::new(),
109 source, disposition,
110 };
111 for token in serialized.split(';') {
113 let token = strip_leading_and_trailing_ascii_whitespace(token);
114 if token.is_empty() { continue };
115 let (directive_name, token) =
116 collect_a_sequence_of_non_ascii_white_space_code_points(token);
117 let mut directive_name = directive_name.to_owned();
118 directive_name.make_ascii_lowercase();
119 if policy.contains_a_directive_whose_name_is(&directive_name) {
120 continue;
121 }
122 let directive_value = split_ascii_whitespace(token).map(String::from).collect();
123 policy.directive_set.push(Directive {
124 name: directive_name,
125 value: directive_value,
126 });
127 }
128 policy
129 }
130 pub fn contains_a_directive_whose_name_is(&self, directive_name: &str) -> bool {
131 self.directive_set.iter().any(|d| d.name == directive_name)
132 }
133 pub fn does_request_violate_policy(&self, request: &Request) -> Violates {
135 if request.initiator == Initiator::Prefetch {
136 return self.does_resource_hint_violate_policy(request);
137 }
138
139 let mut violates = Violates::DoesNotViolate;
140 for directive in &self.directive_set {
141 let result = directive.pre_request_check(request, self);
142 if result == CheckResult::Blocked {
143 violates = Violates::Directive(directive.clone());
144 }
145 }
146 violates
147 }
148
149 pub fn does_resource_hint_violate_policy(&self, request: &Request) -> Violates {
151 let default_directive = &self.directive_set.iter()
152 .find(|x| x.name == "default-src");
153
154 if default_directive.is_none() {
155 return Violates::DoesNotViolate;
156 }
157
158 for directive in &self.directive_set {
159 let result = directive.pre_request_check(request, self);
160 if result == CheckResult::Allowed {
161 return Violates::DoesNotViolate;
162 }
163 }
164
165 return Violates::Directive(default_directive.unwrap().clone());
166 }
167}
168
169#[derive(Clone, Debug)]
170#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
171pub struct CspList(pub Vec<Policy>);
173
174impl Display for CspList {
175 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
176 for (i, directive) in self.0.iter().enumerate() {
177 if i != 0 {
178 write!(f, ",")?;
179 }
180 <Policy as Display>::fmt(directive, f)?;
181 }
182 Ok(())
183 }
184}
185
186static TRUSTED_POLICY_SOURCE_GRAMMAR: Lazy<Regex> =
188 Lazy::new(|| Regex::new(r#"^[0-9a-zA-Z\-\#=_\/@\.%]+$"#).unwrap());
189
190impl CspList {
191 pub fn is_valid(&self) -> bool {
192 self.0.iter().all(Policy::is_valid)
193 }
194 pub fn contains_a_header_delivered_content_security_policy(&self) -> bool {
196 self.0.iter().any(|policy| policy.source == PolicySource::Header)
197 }
198 pub fn parse(list: &str, source: PolicySource, disposition: PolicyDisposition) -> CspList {
200 let mut policies = Vec::new();
201 for token in split_commas(list) {
202 let policy = Policy::parse(token, source, disposition);
203 if policy.directive_set.is_empty() { continue };
204 policies.push(policy)
205 }
206 CspList(policies)
207 }
208 pub fn append(&mut self, mut other: CspList) {
209 self.0.append(&mut other.0)
210 }
211 pub fn push(&mut self, policy: Policy) {
212 self.0.push(policy)
213 }
214 pub fn report_violations_for_request(&self, request: &Request)
220 -> Vec<Violation> {
221 let mut violations = Vec::new();
222 for policy in &self.0 {
223 if policy.disposition == PolicyDisposition::Enforce { continue };
224 let violates = policy.does_request_violate_policy(request);
225 if let Violates::Directive(directive) = violates {
226 let resource = ViolationResource::Url(request.url.clone());
227 violations.push(Violation {
228 resource,
229 directive: Directive {
230 name: get_the_effective_directive_for_request(request).to_owned(),
231 value: directive.value.clone(),
232 },
233 policy: policy.clone(),
234 });
235 }
236 }
237 violations
238 }
239 pub fn should_request_be_blocked(&self, request: &Request) -> (CheckResult, Vec<Violation>) {
246 let mut result = CheckResult::Allowed;
247 let mut violations = Vec::new();
248 for policy in &self.0 {
249 if policy.disposition == PolicyDisposition::Report { continue };
250 let violates = policy.does_request_violate_policy(request);
251 if let Violates::Directive(directive) = violates {
252 result = CheckResult::Blocked;
253 let resource = ViolationResource::Url(request.url.clone());
254 violations.push(Violation {
255 resource,
256 directive: Directive {
257 name: get_the_effective_directive_for_request(request).to_owned(),
258 value: directive.value.clone(),
259 },
260 policy: policy.clone(),
261 });
262 }
263 }
264 (result, violations)
265 }
266 pub fn should_response_to_request_be_blocked(&self, request: &Request, response: &Response)
273 -> (CheckResult, Vec<Violation>) {
274 let mut result = CheckResult::Allowed;
277 let mut violations = Vec::new();
278 for policy in &self.0 {
280 for directive in &policy.directive_set {
282 if directive.post_request_check(request, response, policy) == CheckResult::Blocked {
284 violations.push(Violation {
287 resource: ViolationResource::Url(request.url.clone()),
288 directive: Directive {
289 name: get_the_effective_directive_for_request(request).to_owned(),
290 value: directive.value.clone(),
291 },
292 policy: policy.clone(),
293 });
294 if policy.disposition == PolicyDisposition::Enforce {
296 result = CheckResult::Blocked;
297 }
298 }
299 }
300 }
301 (result, violations)
302 }
303 pub fn should_elements_inline_type_behavior_be_blocked(&self, element: &Element, type_: InlineCheckType, source: &str) -> (CheckResult, Vec<Violation>) {
305 use CheckResult::*;
306 let mut result = Allowed;
307 let mut violations = Vec::new();
308 for policy in &self.0 {
309 for directive in &policy.directive_set {
310 if directive.inline_check(element, type_, policy, source) == Allowed {
311 continue;
312 }
313 let sample = if directive.value.iter().any(|t| &t[..] == "'report-sample'") {
314 let max_length = cmp::min(40, source.len());
315 Some(source[0..max_length].to_owned())
316 } else {
317 None
318 };
319 let violation = Violation {
320 resource: ViolationResource::Inline{ sample },
321 directive: Directive {
322 name: get_the_effective_directive_for_inline_checks(type_).to_owned(),
323 value: directive.value.clone(),
324 },
325 policy: policy.clone(),
326 };
327 violations.push(violation);
328 if policy.disposition == PolicyDisposition::Enforce {
329 result = Blocked;
330 }
331 }
332 }
333 (result, violations)
334 }
335 pub fn is_base_allowed_for_document(&self, base: &Url, self_origin: &Origin) -> (CheckResult, Vec<Violation>) {
342 use CheckResult::*;
343 let mut violations = Vec::new();
344 for policy in &self.0 {
345 let directive = policy.directive_set.iter().find(|directive| directive.name == "base-uri");
346 if let Some(directive) = directive {
347 if SourceList(&directive.value).does_url_match_source_list_in_origin_with_redirect_count(base, &self_origin, 0) == DoesNotMatch {
348 let violation = Violation {
349 directive: directive.clone(),
350 resource: ViolationResource::Inline { sample: None },
351 policy: policy.clone(),
352 };
353 violations.push(violation);
354 if policy.disposition == PolicyDisposition::Enforce {
355 return (Blocked, violations);
356 }
357 }
358 }
359 }
360 return (Allowed, violations);
361 }
362
363 pub fn is_trusted_type_policy_creation_allowed(&self, policy_name: &str, created_policy_names: &[&str]) -> (CheckResult, Vec<Violation>) {
370 use CheckResult::*;
371 let mut result = Allowed;
373 let mut violations = Vec::new();
374 for policy in &self.0 {
376 let mut create_violation = false;
378 let directive = policy.directive_set.iter().find(|directive| directive.name == "trusted-types");
380 if let Some(directive) = directive {
382 if directive.value.len() == 1 && directive.value.contains(&"'none'".to_string()) {
384 create_violation = true;
385 }
386 if created_policy_names.contains(&policy_name) && !directive.value.iter().any(|v| v == "'allow-duplicates'") {
389 create_violation = true;
390 }
391 if !(TRUSTED_POLICY_SOURCE_GRAMMAR.is_match(&policy_name) && (directive.value.iter().any(|p| p == policy_name) || directive.value.iter().any(|v| v == "*"))) {
394 create_violation = true;
395 }
396 if !create_violation {
398 continue;
399 }
400 let max_length = cmp::min(40, policy_name.len());
401 let sample = policy_name[0..max_length].to_owned();
403 let violation = Violation {
406 directive: directive.clone(),
407 resource: ViolationResource::TrustedTypePolicy {
409 sample,
411 },
412 policy: policy.clone(),
413 };
414 violations.push(violation);
416 if policy.disposition == PolicyDisposition::Enforce {
418 result = Blocked
419 }
420 }
421 }
422 return (result, violations);
423 }
424 pub fn does_sink_type_require_trusted_types(&self, sink_group: &str, include_report_only_policies: bool) -> bool {
431 let sink_group = &sink_group.to_owned();
432 for policy in &self.0 {
434 let directive = policy.directive_set.iter().find(|directive| directive.name == "require-trusted-types-for");
436 if let Some(directive) = directive {
438 if !directive.value.contains(sink_group) {
440 continue;
441 }
442 let enforced = policy.disposition == PolicyDisposition::Enforce;
444 if enforced {
446 return true;
447 }
448 if include_report_only_policies {
450 return true;
451 }
452 }
453 }
454 false
456 }
457 pub fn should_sink_type_mismatch_violation_be_blocked_by_csp(&self, sink: &str, sink_group: &str, source: &str) -> (CheckResult, Vec<Violation>) {
464 use CheckResult::*;
465 let sink_group = &sink_group.to_owned();
466 let mut result = Allowed;
468 let mut violations = Vec::new();
469 let mut sample = source;
471 if sink == "Function" {
473 if sample.starts_with("function anonymous") {
475 sample = &sample[18..];
476 } else if sample.starts_with("async function anonymous") {
478 sample = &sample[24..];
479 } else if sample.starts_with("function* anonymous") {
481 sample = &sample[19..];
482 } else if sample.starts_with("async function* anonymous") {
484 sample = &sample[25..];
485 }
486 }
487 for policy in &self.0 {
489 let directive = policy.directive_set.iter().find(|directive| directive.name == "require-trusted-types-for");
491 let Some(directive) = directive else { continue };
493 if !directive.value.contains(sink_group) {
495 continue;
496 }
497 let mut trimmed_sample: String = sample.into();
499 trimmed_sample.truncate(40);
500 violations.push(Violation {
503 resource: ViolationResource::TrustedTypeSink {
505 sample: sink.to_owned() + "|" + &trimmed_sample,
507 },
508 directive: directive.clone(),
509 policy: policy.clone(),
510 });
511 if policy.disposition == PolicyDisposition::Enforce {
513 result = Blocked
514 }
515 }
516 (result, violations)
518 }
519 pub fn get_sandboxing_flag_set_for_document(&self) -> Option<SandboxingFlagSet> {
521 self.0
524 .iter()
525 .flat_map(|policy| {
526 policy.directive_set
527 .iter()
528 .rev()
530 .find(|directive| directive.name == "sandbox")
533 .and_then(|directive| directive.get_sandboxing_flag_set_for_document(policy))
534 })
535 .next()
537 }
538 pub fn is_js_evaluation_allowed(&self, source: &str) -> (CheckResult, Vec<Violation>) {
540 let mut result = CheckResult::Allowed;
541 let mut violations = Vec::new();
542 for policy in &self.0 {
544 let directive = policy.directive_set
546 .iter()
547 .find(|directive| directive.name == "script-src")
550 .or_else(|| policy.directive_set.iter().find(|directive| directive.name == "default-src"));
553 let Some(directive) = directive else { continue };
555 let source_list = SourceList(&directive.value);
556 if source_list.does_a_source_list_allow_js_evaluation() == AllowResult::Allows {
557 continue;
558 }
559 let trusted_types_required = self.does_sink_type_require_trusted_types("'script'", false);
562 if trusted_types_required && directive.value.iter().any(|t| ascii_case_insensitive_match(&t[..], "'trusted-types-eval'")) {
565 continue;
566 }
567 if directive.value.iter().any(|t| ascii_case_insensitive_match(&t[..], "'unsafe-eval'")) {
570 continue;
571 }
572 let sample = if directive.value.iter().any(|t| &t[..] == "'report-sample'") {
575 let max_length = cmp::min(40, source.len());
576 Some(source[0..max_length].to_owned())
577 } else {
578 None
579 };
580 violations.push(Violation {
583 resource: ViolationResource::Eval { sample },
585 directive: directive.clone(),
586 policy: policy.clone(),
587 });
588 if policy.disposition == PolicyDisposition::Enforce {
590 result = CheckResult::Blocked
591 }
592 }
593 (result, violations)
594 }
595 pub fn is_wasm_evaluation_allowed(&self) -> (CheckResult, Vec<Violation>) {
597 let mut result = CheckResult::Allowed;
598 let mut violations = Vec::new();
599 for policy in &self.0 {
601 let directive = policy.directive_set
603 .iter()
604 .find(|directive| directive.name == "script-src")
607 .or_else(|| policy.directive_set.iter().find(|directive| directive.name == "default-src"));
610 let Some(directive) = directive else { continue };
611 let source_list = SourceList(&directive.value);
612 if source_list.does_a_source_list_allow_wasm_evaluation() == AllowResult::Allows {
617 continue;
618 }
619 violations.push(Violation {
622 resource: ViolationResource::WasmEval,
624 directive: directive.clone(),
625 policy: policy.clone(),
626 });
627 if policy.disposition == PolicyDisposition::Enforce {
629 result = CheckResult::Blocked
630 }
631 }
632 (result, violations)
633 }
634 pub fn should_navigation_request_be_blocked<TrustedTypesUrlProcessor>(
645 &self, request: &mut Request, navigation_check_type: NavigationCheckType, mut url_processor: TrustedTypesUrlProcessor) -> (CheckResult, Vec<Violation>)
646 where
647 TrustedTypesUrlProcessor: FnMut(&str) -> Option<String>
648 {
649 let mut result = CheckResult::Allowed;
651 let mut violations = Vec::new();
652 for policy in &self.0 {
654 for directive in &policy.directive_set {
656 if directive.pre_navigation_check(request, navigation_check_type, &mut url_processor, policy) == CheckResult::Allowed {
659 continue;
660 }
661 violations.push(Violation {
665 resource: ViolationResource::Url(request.url.clone()),
667 directive: Directive {
668 name: get_the_effective_directive_for_request(request).to_owned(),
669 value: directive.value.clone(),
670 },
671 policy: policy.clone(),
672 });
673 if policy.disposition == PolicyDisposition::Enforce {
675 result = CheckResult::Blocked;
676 }
677 }
678 }
679 if result == CheckResult::Allowed && request.url.scheme() == "javascript" {
681 for policy in &self.0 {
683 for directive in &policy.directive_set {
685 if directive.inline_check(&Element { nonce: None }, InlineCheckType::Navigation, policy, request.url.as_str()) == CheckResult::Allowed {
688 continue;
689 }
690 violations.push(Violation {
694 resource: ViolationResource::Inline { sample: None },
696 directive: Directive {
697 name: get_the_effective_directive_for_inline_checks(InlineCheckType::Navigation).to_owned(),
700 value: directive.value.clone(),
701 },
702 policy: policy.clone(),
703 });
704 if policy.disposition == PolicyDisposition::Enforce {
706 result = CheckResult::Blocked;
707 }
708 }
709 }
710 }
711 (result, violations)
712 }
713}
714
715#[derive(Clone, Debug)]
716pub struct Element<'a> {
717 pub nonce: Option<Cow<'a, str>>,
723}
724
725#[derive(Clone, Copy, Debug, Eq, PartialEq)]
731pub enum InlineCheckType {
732 Script,
733 ScriptAttribute,
734 Style,
735 StyleAttribute,
736 Navigation,
737}
738
739#[derive(Clone, Copy, Debug, Eq, PartialEq)]
745pub enum NavigationCheckType {
746 FormSubmission,
747 Other,
748}
749
750#[derive(Clone, Debug)]
756pub struct Request {
757 pub url: Url,
758 pub origin: Origin,
759 pub redirect_count: u32,
760 pub destination: Destination,
761 pub initiator: Initiator,
762 pub nonce: String,
763 pub integrity_metadata: String,
764 pub parser_metadata: ParserMetadata,
765}
766
767#[derive(Clone, Copy, Debug, Eq, PartialEq)]
768pub enum ParserMetadata {
769 ParserInserted,
770 NotParserInserted,
771 None,
772}
773
774#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
775#[derive(Clone, Copy, Debug, Eq, PartialEq)]
776pub enum Initiator {
777 Download,
778 ImageSet,
779 Manifest,
780 Prefetch,
781 Prerender,
782 Fetch,
783 Xslt,
784 None,
785}
786
787#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
788#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
789pub enum Destination {
790 None,
791 Audio,
792 AudioWorklet,
793 Document,
794 Embed,
795 Font,
796 Frame,
797 IFrame,
798 Image,
799 Json,
800 Manifest,
801 Object,
802 PaintWorklet,
803 Report,
804 Script,
805 ServiceWorker,
806 SharedWorker,
807 Style,
808 Track,
809 Video,
810 WebIdentity,
811 Worker,
812 Xslt,
813}
814
815pub struct InvalidDestination;
816
817impl FromStr for Destination {
818 type Err = InvalidDestination;
819
820 fn from_str(s: &str) -> Result<Self, Self::Err> {
821 let destination = match s {
822 "" => Self::None,
823 "audio" => Self::Audio,
824 "audioworklet" => Self::AudioWorklet,
825 "document" => Self::Document,
826 "embed" => Self::Embed,
827 "font" => Self::Font,
828 "frame" => Self::Frame,
829 "iframe" => Self::IFrame,
830 "image" => Self::Image,
831 "json" => Self::Json,
832 "manifest" => Self::Manifest,
833 "object" => Self::Object,
834 "paintworklet" => Self::PaintWorklet,
835 "report" => Self::Report,
836 "script" => Self::Script,
837 "serviceworker" => Self::ServiceWorker,
838 "sharedworker" => Self::SharedWorker,
839 "style" => Self::Style,
840 "track" => Self::Track,
841 "video" => Self::Video,
842 "webidentity" => Self::WebIdentity,
843 "worker" => Self::Worker,
844 "xslt" => Self::Xslt,
845 _ => return Err(InvalidDestination),
846 };
847
848 Ok(destination)
849 }
850}
851
852impl Destination {
853 pub fn is_script_like(self) -> bool {
855 use Destination::*;
856 matches!(self, AudioWorklet | PaintWorklet | Script | ServiceWorker | SharedWorker | Worker | Xslt)
857 }
858
859 pub const fn as_str(&self) -> &'static str {
860 match self {
861 Self::None => "",
862 Self::Audio => "audio",
863 Self::AudioWorklet => "audioworklet",
864 Self::Document => "document",
865 Self::Embed => "embed",
866 Self::Font => "font",
867 Self::Frame => "frame",
868 Self::IFrame => "iframe",
869 Self::Image => "image",
870 Self::Json => "json",
871 Self::Manifest => "manifest",
872 Self::Object => "object",
873 Self::PaintWorklet => "paintworklet",
874 Self::Report => "report",
875 Self::Script => "script",
876 Self::ServiceWorker => "serviceworker",
877 Self::SharedWorker => "sharedworker",
878 Self::Style => "style",
879 Self::Track => "track",
880 Self::Video => "video",
881 Self::WebIdentity => "webidentity",
882 Self::Worker => "worker",
883 Self::Xslt => "xslt",
884 }
885 }
886}
887
888#[derive(Clone, Debug)]
893pub struct Response {
894 pub url: Url,
895 pub redirect_count: u32,
896}
897
898#[derive(Clone, Debug)]
904#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
905pub struct Violation {
906 pub resource: ViolationResource,
907 pub directive: Directive,
908 pub policy: Policy,
909}
910
911#[derive(Clone, Debug)]
917#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
918pub enum ViolationResource {
919 Url(Url),
920 Inline {
921 sample: Option<String>,
922 },
923 TrustedTypePolicy {
924 sample: String,
925 },
926 TrustedTypeSink {
927 sample: String,
928 },
929 Eval {
930 sample: Option<String>,
931 },
932 WasmEval,
933}
934
935#[derive(Clone, Debug, Eq, PartialEq)]
940#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
941pub enum CheckResult {
942 Allowed,
943 Blocked,
944}
945
946#[derive(Clone, Debug, Eq, PartialEq)]
950#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
951pub enum Violates {
952 DoesNotViolate,
953 Directive(Directive),
954}
955
956#[derive(Clone, Copy, Debug, Eq, PartialEq)]
958#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
959pub enum PolicyDisposition {
960 Enforce,
961 Report,
962}
963
964#[derive(Clone, Copy, Debug, Eq, PartialEq)]
966#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
967pub enum PolicySource {
968 Header,
969 Meta,
970}
971
972#[derive(Clone, Debug, Eq, PartialEq)]
974#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
975pub struct Directive {
976 pub name: String,
977 pub value: Vec<String>,
978}
979
980impl Display for Directive {
981 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
982 <str as Display>::fmt(&self.name[..], f)?;
983 write!(f, " ")?;
984 for (i, token) in self.value.iter().enumerate() {
985 if i != 0 {
986 write!(f, " ")?;
987 }
988 <str as Display>::fmt(&token[..], f)?;
989 }
990 Ok(())
991 }
992}
993
994impl Directive {
995 pub fn is_valid(&self) -> bool {
997 DIRECTIVE_NAME_GRAMMAR.is_match(&self.name) &&
998 self.value.iter().all(|t| DIRECTIVE_VALUE_TOKEN_GRAMMAR.is_match(&t[..]))
999 }
1000 pub fn pre_request_check(&self, request: &Request, policy: &Policy) -> CheckResult {
1002 use CheckResult::*;
1003 match &self.name[..] {
1004 "child-src" => {
1005 let name = get_the_effective_directive_for_request(request);
1006 if !should_fetch_directive_execute(name, "child-src", policy) {
1007 return Allowed;
1008 }
1009 (Directive {
1010 name: String::from(name),
1011 value: self.value.clone(),
1012 }).pre_request_check(request, policy)
1013 },
1014 "connect-src" => {
1015 let name = get_the_effective_directive_for_request(request);
1016 if !should_fetch_directive_execute(name, "connect-src", policy) {
1017 return Allowed;
1018 }
1019 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1020 return Blocked;
1021 }
1022 Allowed
1023 }
1024 "default-src" => {
1025 let name = get_the_effective_directive_for_request(request);
1026 if !should_fetch_directive_execute(name, "default-src", policy) {
1027 return Allowed;
1028 }
1029 (Directive {
1030 name: String::from(name),
1031 value: self.value.clone(),
1032 }).pre_request_check(request, policy)
1033 }
1034 "font-src" => {
1035 let name = get_the_effective_directive_for_request(request);
1036 if !should_fetch_directive_execute(name, "font-src", policy) {
1037 return Allowed;
1038 }
1039 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1040 return Blocked;
1041 }
1042 Allowed
1043 }
1044 "frame-src" => {
1045 let name = get_the_effective_directive_for_request(request);
1046 if !should_fetch_directive_execute(name, "frame-src", policy) {
1047 return Allowed;
1048 }
1049 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1050 return Blocked;
1051 }
1052 Allowed
1053 }
1054 "img-src" => {
1055 let name = get_the_effective_directive_for_request(request);
1056 if !should_fetch_directive_execute(name, "img-src", policy) {
1057 return Allowed;
1058 }
1059 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1060 return Blocked;
1061 }
1062 Allowed
1063 }
1064 "manifest-src" => {
1065 let name = get_the_effective_directive_for_request(request);
1066 if !should_fetch_directive_execute(name, "manifest-src", policy) {
1067 return Allowed;
1068 }
1069 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1070 return Blocked;
1071 }
1072 Allowed
1073 }
1074 "media-src" => {
1075 let name = get_the_effective_directive_for_request(request);
1076 if !should_fetch_directive_execute(name, "media-src", policy) {
1077 return Allowed;
1078 }
1079 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1080 return Blocked;
1081 }
1082 Allowed
1083 }
1084 "object-src" => {
1085 let name = get_the_effective_directive_for_request(request);
1086 if !should_fetch_directive_execute(name, "object-src", policy) {
1087 return Allowed;
1088 }
1089 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1090 return Blocked;
1091 }
1092 Allowed
1093 }
1094 "script-src" => {
1095 let name = get_the_effective_directive_for_request(request);
1096 if !should_fetch_directive_execute(name, "script-src", policy) {
1097 return Allowed;
1098 }
1099 script_directives_prerequest_check(request, self)
1100 }
1101 "script-src-elem" => {
1102 let name = get_the_effective_directive_for_request(request);
1103 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1104 return Allowed;
1105 }
1106 script_directives_prerequest_check(request, self)
1107 }
1108 "style-src" => {
1109 let name = get_the_effective_directive_for_request(request);
1110 if !should_fetch_directive_execute(name, "style-src", policy) {
1111 return Allowed;
1112 }
1113 let source_list = SourceList(&self.value);
1114 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1115 return Allowed;
1116 }
1117 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1118 return Blocked;
1119 }
1120 Allowed
1121 }
1122 "style-src-elem" => {
1123 let name = get_the_effective_directive_for_request(request);
1124 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1125 return Allowed;
1126 }
1127 let source_list = SourceList(&self.value);
1128 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1129 return Allowed;
1130 }
1131 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1132 return Blocked;
1133 }
1134 Allowed
1135 }
1136 "worker-src" => {
1137 let name = get_the_effective_directive_for_request(request);
1138 if !should_fetch_directive_execute(name, "worker-src", policy) {
1139 return Allowed;
1140 }
1141 let source_list = SourceList(&self.value);
1142 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1143 return Blocked;
1144 }
1145 Allowed
1146 }
1147 _ => Allowed,
1148 }
1149 }
1150 pub fn post_request_check(&self, request: &Request, response: &Response, policy: &Policy) -> CheckResult {
1152 use CheckResult::*;
1153 match &self.name[..] {
1154 "child-src" => {
1155 let name = get_the_effective_directive_for_request(request);
1156 if !should_fetch_directive_execute(name, "child-src", policy) {
1157 return Allowed;
1158 }
1159 Directive {
1160 name: name.to_owned(),
1161 value: self.value.clone()
1162 }.post_request_check(request, response, policy)
1163 }
1164 "connect-src" => {
1165 let name = get_the_effective_directive_for_request(request);
1166 if !should_fetch_directive_execute(name, "connect-src", policy) {
1167 return Allowed;
1168 }
1169 let source_list = SourceList(&self.value);
1170 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1171 return Blocked;
1172 }
1173 Allowed
1174 }
1175 "default-src" => {
1176 let name = get_the_effective_directive_for_request(request);
1177 if !should_fetch_directive_execute(name, "default-src", policy) {
1178 return Allowed;
1179 }
1180 Directive {
1181 name: name.to_owned(),
1182 value: self.value.clone(),
1183 }.post_request_check(request, response, policy)
1184 }
1185 "font-src" => {
1186 let name = get_the_effective_directive_for_request(request);
1187 if !should_fetch_directive_execute(name, "font-src", policy) {
1188 return Allowed;
1189 }
1190 let source_list = SourceList(&self.value);
1191 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1192 return Blocked;
1193 }
1194 Allowed
1195 }
1196 "frame-src" => {
1197 let name = get_the_effective_directive_for_request(request);
1198 if !should_fetch_directive_execute(name, "frame-src", policy) {
1199 return Allowed;
1200 }
1201 let source_list = SourceList(&self.value);
1202 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1203 return Blocked;
1204 }
1205 Allowed
1206 }
1207 "img-src" => {
1208 let name = get_the_effective_directive_for_request(request);
1209 if !should_fetch_directive_execute(name, "img-src", policy) {
1210 return Allowed;
1211 }
1212 let source_list = SourceList(&self.value);
1213 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1214 return Blocked;
1215 }
1216 Allowed
1217 }
1218 "manifest-src" => {
1219 let name = get_the_effective_directive_for_request(request);
1220 if !should_fetch_directive_execute(name, "manifest-src", policy) {
1221 return Allowed;
1222 }
1223 let source_list = SourceList(&self.value);
1224 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1225 return Blocked;
1226 }
1227 Allowed
1228 }
1229 "media-src" => {
1230 let name = get_the_effective_directive_for_request(request);
1231 if !should_fetch_directive_execute(name, "media-src", policy) {
1232 return Allowed;
1233 }
1234 let source_list = SourceList(&self.value);
1235 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1236 return Blocked;
1237 }
1238 Allowed
1239 }
1240 "object-src" => {
1241 let name = get_the_effective_directive_for_request(request);
1242 if !should_fetch_directive_execute(name, "object-src", policy) {
1243 return Allowed;
1244 }
1245 let source_list = SourceList(&self.value);
1246 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1247 return Blocked;
1248 }
1249 Allowed
1250 }
1251 "script-src" => {
1252 let name = get_the_effective_directive_for_request(request);
1253 if !should_fetch_directive_execute(name, "script-src", policy) {
1254 return Allowed;
1255 }
1256 script_directives_postrequest_check(request, response, self)
1257 }
1258 "script-src-elem" => {
1259 let name = get_the_effective_directive_for_request(request);
1260 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1261 return Allowed;
1262 }
1263 script_directives_postrequest_check(request, response, self)
1264 }
1265 "style-src" => {
1266 let name = get_the_effective_directive_for_request(request);
1267 if !should_fetch_directive_execute(name, "style-src", policy) {
1268 return Allowed;
1269 }
1270 let source_list = SourceList(&self.value);
1271 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1272 return Allowed;
1273 }
1274 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1275 return Blocked;
1276 }
1277 Allowed
1278 }
1279 "style-src-elem" => {
1280 let name = get_the_effective_directive_for_request(request);
1281 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1282 return Allowed;
1283 }
1284 let source_list = SourceList(&self.value);
1285 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1286 return Allowed;
1287 }
1288 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1289 return Blocked;
1290 }
1291 Allowed
1292 }
1293 "worker-src" => {
1294 let name = get_the_effective_directive_for_request(request);
1295 if !should_fetch_directive_execute(name, "worker-src", policy) {
1296 return Allowed;
1297 }
1298 let source_list = SourceList(&self.value);
1299 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1300 return Blocked;
1301 }
1302 Allowed
1303 }
1304 _ => Allowed,
1305 }
1306 }
1307 pub fn inline_check(&self, element: &Element, type_: InlineCheckType, policy: &Policy, source: &str) -> CheckResult {
1309 use CheckResult::*;
1310 match &self.name[..] {
1311 "default-src" => {
1312 let name = get_the_effective_directive_for_inline_checks(type_);
1313 if !should_fetch_directive_execute(name, "default-src", policy) {
1314 return Allowed;
1315 }
1316 Directive {
1317 name: name.to_owned(),
1318 value: self.value.clone()
1319 }.inline_check(element, type_, policy, source)
1320 }
1321 "script-src" => {
1322 let name = get_the_effective_directive_for_inline_checks(type_);
1323 if !should_fetch_directive_execute(name, "script-src", policy) {
1324 return Allowed;
1325 }
1326 let source_list = SourceList(&self.value);
1327 if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1328 return Blocked;
1329 }
1330 Allowed
1331 }
1332 "script-src-elem" => {
1333 let name = get_the_effective_directive_for_inline_checks(type_);
1334 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1335 return Allowed;
1336 }
1337 let source_list = SourceList(&self.value);
1338 if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1339 return Blocked;
1340 }
1341 Allowed
1342 }
1343 "script-src-attr" => {
1344 let name = get_the_effective_directive_for_inline_checks(type_);
1345 if !should_fetch_directive_execute(name, "script-src-attr", policy) {
1346 return Allowed;
1347 }
1348 let source_list = SourceList(&self.value);
1349 if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1350 return Blocked;
1351 }
1352 Allowed
1353 }
1354 "style-src" => {
1355 let name = get_the_effective_directive_for_inline_checks(type_);
1356 if !should_fetch_directive_execute(name, "style-src", policy) {
1357 return Allowed;
1358 }
1359 let source_list = SourceList(&self.value);
1360 if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1361 return Blocked;
1362 }
1363 Allowed
1364 }
1365 "style-src-elem" => {
1366 let name = get_the_effective_directive_for_inline_checks(type_);
1367 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1368 return Allowed;
1369 }
1370 let source_list = SourceList(&self.value);
1371 if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1372 return Blocked;
1373 }
1374 Allowed
1375 }
1376 "style-src-attr" => {
1377 let name = get_the_effective_directive_for_inline_checks(type_);
1378 if !should_fetch_directive_execute(name, "style-src-attr", policy) {
1379 return Allowed;
1380 }
1381 let source_list = SourceList(&self.value);
1382 if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1383 return Blocked;
1384 }
1385 Allowed
1386 }
1387 _ => Allowed
1388 }
1389 }
1390 pub fn get_sandboxing_flag_set_for_document(&self, policy: &Policy) -> Option<SandboxingFlagSet> {
1392 debug_assert!(&self.name[..] == "sandbox");
1393 if policy.disposition != PolicyDisposition::Enforce {
1395 None
1396 } else {
1397 Some(parse_a_sandboxing_directive(&self.value[..]))
1399 }
1400 }
1401 pub fn pre_navigation_check<TrustedTypesUrlProcessor>(
1403 &self, request: &mut Request, type_: NavigationCheckType, mut url_processor: TrustedTypesUrlProcessor, _policy: &Policy) -> CheckResult
1404 where
1405 TrustedTypesUrlProcessor: FnMut(&str) -> Option<String>
1406 {
1407 use CheckResult::*;
1408 match &self.name[..] {
1409 "form-action" => {
1411 if type_ == NavigationCheckType::FormSubmission {
1413 let source_list = SourceList(&self.value);
1414 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1417 return Blocked;
1418 }
1419 }
1420 Allowed
1422 },
1423 "require-trusted-types-for" => {
1425 let url = &request.url;
1426 if url.scheme() != "javascript" {
1428 return Allowed;
1429 }
1430 let encoded_script_source = &url[Position::AfterScheme..][1..];
1435 let Some(converted_script_source) = url_processor(encoded_script_source) else {
1439 return Blocked;
1440 };
1441 let url_string = "javascript:".to_owned() + &converted_script_source;
1443 let Ok(new_url) = Url::parse(&url_string) else {
1446 return Blocked;
1447 };
1448 request.url = new_url;
1450 Allowed
1452 }
1453 _ => Allowed,
1454 }
1455 }
1456}
1457
1458fn get_the_effective_directive_for_inline_checks(type_: InlineCheckType) -> &'static str {
1460 use InlineCheckType::*;
1461 match type_ {
1462 Script | Navigation => "script-src-elem",
1463 ScriptAttribute => "script-src-attr",
1464 Style => "style-src-elem",
1465 StyleAttribute => "style-src-attr",
1466 }
1467}
1468
1469fn script_directives_prerequest_check(request: &Request, directive: &Directive) -> CheckResult {
1471 use CheckResult::*;
1472 if request_is_script_like(request) {
1474 let source_list = SourceList(&directive.value[..]);
1475 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1478 return Allowed;
1479 }
1480 if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata) == Matches {
1483 return Allowed;
1484 }
1485 if directive.value.iter().any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'")) {
1488 if request.parser_metadata == ParserMetadata::ParserInserted {
1490 return Blocked;
1491 }
1492 return Allowed;
1494 }
1495
1496 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1499 return Blocked;
1500 }
1501 }
1502 Allowed
1504}
1505
1506fn script_directives_postrequest_check(request: &Request, response: &Response, directive: &Directive) -> CheckResult {
1508 use CheckResult::*;
1509 if request_is_script_like(request) {
1511 let source_list = SourceList(&directive.value[..]);
1514 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1517 return Allowed;
1518 }
1519 if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata) == Matches {
1522 return Allowed;
1523 }
1524 if directive.value.iter().any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'")) {
1526 if request.parser_metadata == ParserMetadata::ParserInserted {
1528 return Blocked;
1529 }
1530 return Allowed;
1532 }
1533 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1536 return Blocked;
1537 }
1538 }
1539 Allowed
1541}
1542
1543fn request_is_script_like(request: &Request) -> bool {
1545 request.destination.is_script_like()
1546}
1547
1548fn should_fetch_directive_execute(effective_directive_name: &str, directive_name: &str, policy: &Policy) -> bool {
1550 let directive_fallback_list = get_fetch_directive_fallback_list(effective_directive_name);
1551 for fallback_directive in directive_fallback_list {
1552 if directive_name == *fallback_directive {
1553 return true;
1554 }
1555 if policy.contains_a_directive_whose_name_is(fallback_directive) {
1556 return false;
1557 }
1558 }
1559 false
1560}
1561
1562fn get_fetch_directive_fallback_list(directive_name: &str) -> &'static [&'static str] {
1564 match directive_name {
1565 "script-src-elem" => &["script-src-elem", "script-src", "default-src"],
1566 "script-src-attr" => &["script-src-attr", "script-src", "default-src"],
1567 "style-src-elem" => &["style-src-elem", "style-src", "default-src"],
1568 "style-src-attr" => &["style-src-attr", "style-src", "default-src"],
1569 "worker-src" => &["worker-src", "child-src", "script-src", "default-src"],
1570 "connect-src" => &["connect-src", "default-src"],
1571 "manifest-src" => &["manifest-src", "default-src"],
1572 "object-src" => &["object-src", "default-src"],
1573 "frame-src" => &["frame-src", "child-src", "default-src"],
1574 "media-src" => &["media-src", "default-src"],
1575 "font-src" => &["font-src", "default-src"],
1576 "img-src" => &["img-src", "default-src"],
1577 _ => &[],
1578 }
1579}
1580
1581fn get_the_effective_directive_for_request(request: &Request) -> &'static str {
1583 use Initiator::*;
1584 use Destination::*;
1585 if request.initiator == Prefetch || request.initiator == Prerender {
1587 return "default-src";
1588 }
1589 match request.destination {
1591 Destination::Manifest => "manifest-src",
1592 Object | Embed => "object-src",
1593 Frame | IFrame => "frame-src",
1594 Audio | Track | Video => "media-src",
1595 Font => "font-src",
1596 Image => "img-src",
1597 Style => "style-src-elem",
1598 Script | Destination::Xslt | AudioWorklet | PaintWorklet => "script-src-elem",
1599 ServiceWorker | SharedWorker | Worker => "worker-src",
1600 Json | WebIdentity => "connect-src",
1601 Report => "",
1602 _ => "connect-src",
1604 }
1605}
1606
1607#[derive(Clone, Debug, Eq, PartialEq)]
1609pub enum MatchResult {
1610 Matches,
1611 DoesNotMatch,
1612}
1613
1614static DIRECTIVE_NAME_GRAMMAR: Lazy<Regex> =
1616 Lazy::new(|| Regex::new(r#"^[0-9a-z\-]+$"#).unwrap());
1617static DIRECTIVE_VALUE_TOKEN_GRAMMAR: Lazy<Regex> =
1619 Lazy::new(|| Regex::new(r#"^[\u{21}-\u{2B}\u{2D}-\u{3A}\u{3C}-\u{7E}]+$"#).unwrap());
1620static NONCE_SOURCE_GRAMMAR: Lazy<Regex> =
1622 Lazy::new(|| Regex::new(r#"^'nonce-(?P<n>[a-zA-Z0-9\+/\-_]+=*)'$"#).unwrap());
1623static NONE_SOURCE_GRAMMAR: Lazy<Regex> =
1624 Lazy::new(|| Regex::new(r#"^'none'$"#).unwrap());
1625static SCHEME_SOURCE_GRAMMAR: Lazy<Regex> =
1627 Lazy::new(|| Regex::new(r#"^(?P<scheme>[a-zA-Z][a-zA-Z0-9\+\-\.]*):$"#).unwrap());
1628static HOST_SOURCE_GRAMMAR: Lazy<Regex> =
1630 Lazy::new(|| 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());
1631static HASH_SOURCE_GRAMMAR: Lazy<Regex> =
1633 Lazy::new(|| Regex::new(r#"^'(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)'$"#).unwrap());
1634
1635#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1637struct SourceList<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item=&'a U>>(I);
1638
1639impl<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item=&'a U>> SourceList<'a, U, I> {
1640 fn does_nonce_match_source_list(&self, nonce: &str) -> MatchResult {
1642 if nonce.is_empty() { return DoesNotMatch };
1643 for expression in self.0.clone().into_iter() {
1644 if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression.borrow()) {
1645 if let Some(captured_nonce) = captures.name("n") {
1646 if nonce == captured_nonce.as_str() {
1647 return Matches;
1648 }
1649 }
1650 }
1651 }
1652 DoesNotMatch
1653 }
1654 fn does_integrity_metadata_match_source_list(&self, integrity_metadata: &str) -> MatchResult {
1656 let integrity_expressions: Vec<HashFunction> = self.0.clone().into_iter()
1658 .filter_map(|expression| {
1659 if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression.borrow()) {
1660 if let (Some(algorithm), Some(value)) = (captures.name("algorithm").and_then(|a| HashAlgorithm::from_name(a.as_str())), captures.name("value")) {
1661 return Some(HashFunction{ algorithm, value: String::from(value.as_str()) });
1662 }
1663 }
1664 None
1665 })
1666 .collect();
1667 if integrity_expressions.is_empty() {
1669 return DoesNotMatch;
1670 }
1671 let integrity_sources = parse_subresource_integrity_metadata(integrity_metadata);
1673 match integrity_sources {
1674 SubresourceIntegrityMetadata::NoMetadata => DoesNotMatch,
1676 SubresourceIntegrityMetadata::IntegritySources(integrity_sources) => {
1677 if integrity_sources.is_empty() {
1678 return DoesNotMatch;
1679 }
1680 for source in &integrity_sources {
1682 if !integrity_expressions.iter().any(|ex| ex == source) {
1689 return DoesNotMatch;
1690 }
1691 }
1692 Matches
1694 }
1695 }
1696 }
1697 fn does_request_match_source_list(&self, request: &Request) -> MatchResult {
1699 self.does_url_match_source_list_in_origin_with_redirect_count(
1700 &request.url,
1701 &request.origin,
1702 request.redirect_count,
1703 )
1704 }
1705 fn does_url_match_source_list_in_origin_with_redirect_count(
1707 &self,
1708 url: &Url,
1709 origin: &Origin,
1710 redirect_count: u32,
1711 ) -> MatchResult {
1712 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1713 if NONE_SOURCE_GRAMMAR.is_match(expression) { continue };
1714 let result = does_url_match_expression_in_origin_with_redirect_count(
1715 url,
1716 expression,
1717 origin,
1718 redirect_count
1719 );
1720 if result == Matches {
1721 return Matches;
1722 }
1723 }
1724 DoesNotMatch
1725 }
1726 fn does_element_match_source_list_for_type_and_source(
1728 &self,
1729 element: &Element,
1730 type_: InlineCheckType,
1731 source: &str,
1732 ) -> MatchResult {
1733 if self.does_a_source_list_allow_all_inline_behavior_for_type(type_) == AllowResult::Allows {
1734 return Matches;
1735 }
1736 if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style {
1737 if let Some(nonce) = element.nonce.as_ref() {
1738 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1739 if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression) {
1740 if let Some(captured_nonce) = captures.name("n") {
1741 if nonce == captured_nonce.as_str() {
1742 return Matches;
1743 }
1744 }
1745 }
1746 }
1747 }
1748 }
1749 let mut unsafe_hashes = false;
1750 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1751 if ascii_case_insensitive_match(expression, "'unsafe-hashes'") {
1752 unsafe_hashes = true;
1753 break;
1754 }
1755 }
1756 if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style || unsafe_hashes {
1757 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1758 if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression) {
1759 if let (Some(algorithm), Some(value)) = (captures.name("algorithm").and_then(|a| HashAlgorithm::from_name(a.as_str())), captures.name("value")) {
1760 let actual = algorithm.apply(source);
1761 let expected = value.as_str().replace('-', "+").replace('_', "/");
1762 if actual == expected {
1763 return Matches;
1764 }
1765 }
1766 }
1767 }
1768 }
1769 DoesNotMatch
1770 }
1771 fn does_a_source_list_allow_all_inline_behavior_for_type(&self, type_: InlineCheckType) -> AllowResult {
1773 use InlineCheckType::*;
1774 let mut allow_all_inline = false;
1775 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1776 if HASH_SOURCE_GRAMMAR.is_match(expression) || NONCE_SOURCE_GRAMMAR.is_match(expression) {
1777 return AllowResult::DoesNotAllow;
1778 }
1779 if (type_ == Script || type_ == ScriptAttribute || type_ == Navigation) && expression == "'strict-dynamic'" {
1780 return AllowResult::DoesNotAllow;
1781 }
1782 if ascii_case_insensitive_match(expression, "'unsafe-inline'") {
1783 allow_all_inline = true;
1784 }
1785 }
1786 if allow_all_inline {
1787 AllowResult::Allows
1788 } else {
1789 AllowResult::DoesNotAllow
1790 }
1791 }
1792 fn does_response_to_request_match_source_list(
1794 &self,
1795 request: &Request,
1796 response: &Response) -> MatchResult {
1797 self.does_url_match_source_list_in_origin_with_redirect_count(
1798 &response.url,
1799 &request.origin,
1800 response.redirect_count,
1801 )
1802 }
1803 fn does_a_source_list_allow_js_evaluation(&self) -> AllowResult {
1805 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1806 if ascii_case_insensitive_match(expression, "'unsafe-eval'") {
1809 return AllowResult::Allows;
1810 }
1811 }
1812 AllowResult::DoesNotAllow
1813 }
1814 fn does_a_source_list_allow_wasm_evaluation(&self) -> AllowResult {
1816 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1817 if ascii_case_insensitive_match(expression, "'unsafe-eval'") ||
1818 ascii_case_insensitive_match(expression, "'wasm-unsafe-eval'") {
1819 return AllowResult::Allows;
1820 }
1821 }
1822 AllowResult::DoesNotAllow
1823 }
1824}
1825
1826#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1828enum AllowResult {
1829 Allows,
1830 DoesNotAllow,
1831}
1832
1833fn does_url_match_expression_in_origin_with_redirect_count(
1835 url: &Url,
1836 expression: &str,
1837 origin: &Origin,
1838 redirect_count: u32,
1839) -> MatchResult {
1840 let url_scheme = url.scheme();
1841 if expression == "*" {
1842 if scheme_is_network(url_scheme) {
1843 return Matches;
1844 }
1845 return origin_scheme_part_match(origin, url_scheme);
1846 }
1847 if let Some(captures) = SCHEME_SOURCE_GRAMMAR.captures(expression) {
1848 if let Some(expression_scheme) = captures.name("scheme") {
1849 return scheme_part_match(expression_scheme.as_str(), url_scheme);
1850 }
1851 return DoesNotMatch;
1853 }
1854 if let Some(captures) = HOST_SOURCE_GRAMMAR.captures(expression) {
1855 let expr_has_scheme_part = if let Some(expression_scheme) = captures.name("scheme") {
1856 if scheme_part_match(expression_scheme.as_str(), url_scheme) != Matches {
1857 return DoesNotMatch;
1858 }
1859 true
1860 } else {
1861 false
1862 };
1863 let url_host = if let Some(url_host) = url.host() {
1864 url_host
1865 } else {
1866 return DoesNotMatch;
1867 };
1868 if !expr_has_scheme_part &&
1869 origin_scheme_part_match(origin, url.scheme()) != Matches {
1870 return DoesNotMatch;
1871 }
1872 if let Some(expression_host) = captures.name("host") {
1873 if host_part_match(expression_host.as_str(), &url_host.to_string()) != Matches {
1874 return DoesNotMatch;
1875 }
1876 } else {
1877 return DoesNotMatch;
1879 }
1880 let port_part = captures.name("port").map(|port| &port.as_str()[1..]);
1882 if port_part_match(port_part, url) != Matches {
1883 return DoesNotMatch;
1884 }
1885 let path_part = captures.name("path").map(|path_part| path_part.as_str()).unwrap_or("");
1886 if path_part != "/" && redirect_count == 0 {
1887 let path = url.path();
1888 if path_part_match(path_part, path) != Matches {
1889 return DoesNotMatch;
1890 }
1891 }
1892 return Matches;
1893 }
1894 if ascii_case_insensitive_match(expression, "'self'") {
1895 if *origin == url.origin() {
1896 return Matches;
1897 }
1898 if let Origin::Tuple(scheme, host, port) = origin {
1899 let hosts_are_the_same = Some(host) == url.host().map(|p| p.to_owned()).as_ref();
1900 let ports_are_the_same = Some(*port) == url.port();
1901 let origins_port_is_default_for_scheme = Some(*port) == default_port(scheme);
1902 let url_port_is_default_port_for_scheme = url.port() == default_port(scheme)
1903 && default_port(scheme).is_some();
1904 let ports_are_default = url_port_is_default_port_for_scheme && origins_port_is_default_for_scheme;
1905 if hosts_are_the_same
1906 && (ports_are_the_same || ports_are_default)
1907 && ((url_scheme == "https" || url_scheme == "wss")
1908 || (scheme == "http" && (url_scheme == "http" || url_scheme == "ws"))) {
1909 return Matches;
1910 }
1911 }
1912 }
1913 DoesNotMatch
1914}
1915
1916fn host_part_match(pattern: &str, host: &str) -> MatchResult {
1918 debug_assert!(!host.is_empty());
1919 if host.is_empty() {
1921 return DoesNotMatch;
1922 }
1923 if pattern.as_bytes()[0] == b'*' {
1924 if pattern.len() == 1 {
1926 return Matches;
1927 }
1928 if pattern.as_bytes()[1] == b'.' {
1930 let remaining_pattern = &pattern[1..];
1932 if remaining_pattern.len() > host.len() {
1933 return DoesNotMatch;
1934 }
1935 let remaining_host = &host[(host.len()-remaining_pattern.len())..];
1936 debug_assert_eq!(remaining_host.len(), remaining_pattern.len());
1937 if ascii_case_insensitive_match(remaining_pattern, remaining_host) {
1939 return Matches;
1940 }
1941 return DoesNotMatch;
1943 }
1944 }
1945 if !ascii_case_insensitive_match(pattern, host) {
1947 return DoesNotMatch;
1948 }
1949 static IPV4_ADDRESS_RULE: Lazy<Regex> =
1950 Lazy::new(|| 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());
1951 if IPV4_ADDRESS_RULE.is_match(pattern) && pattern != "127.0.0.1" {
1952 return DoesNotMatch;
1953 }
1954 if pattern.as_bytes()[0] == b'[' {
1958 return DoesNotMatch;
1959 }
1960 Matches
1962}
1963
1964fn port_part_match(input: Option<&str>, url: &Url) -> MatchResult {
1966 use std::str::FromStr;
1967 debug_assert!(input.is_none() || input == Some("*") || u16::from_str(input.unwrap()).is_ok());
1969 if input == Some("*") {
1971 return Matches;
1972 }
1973 let normalized_input = if let Some(input) = input {
1975 u16::from_str(&input).ok()
1976 } else {
1977 None
1978 };
1979 if normalized_input == url.port() {
1981 return Matches;
1982 }
1983 if url.port().is_none() {
1985 let default_port = default_port(url.scheme());
1987 if normalized_input == default_port {
1989 return Matches;
1990 }
1991 }
1992 DoesNotMatch
1994}
1995
1996fn path_part_match(path_a: &str, path_b: &str) -> MatchResult {
1998 if path_a.is_empty() {
1999 return Matches;
2000 }
2001 if path_a == "/" && path_b.is_empty() {
2002 return Matches;
2003 }
2004 let exact_match = path_a.as_bytes()[path_a.len()-1] != b'/';
2005 let (mut path_list_a, path_list_b): (Vec<&str>, Vec<&str>) =
2006 (path_a.split('/').collect(), path_b.split('/').collect());
2007 if path_list_a.len() > path_list_b.len() {
2008 return DoesNotMatch;
2009 }
2010 if exact_match && path_list_a.len() != path_list_b.len() {
2011 return DoesNotMatch;
2012 }
2013 if !exact_match {
2014 debug_assert_eq!(path_list_a[path_list_a.len()-1], "");
2015 path_list_a.pop();
2016 }
2017 let mut piece_b_iter = path_list_b.iter();
2018 for piece_a in &path_list_a {
2019 let piece_b = piece_b_iter.next().unwrap();
2020 let piece_a: Vec<u8> = percent_encoding::percent_decode(piece_a.as_bytes()).collect();
2021 let piece_b: Vec<u8> = percent_encoding::percent_decode(piece_b.as_bytes()).collect();
2022 if piece_a != piece_b {
2023 return DoesNotMatch;
2024 }
2025 }
2026 Matches
2027}
2028
2029fn default_port(scheme: &str) -> Option<u16> {
2030 Some(match scheme {
2031 "ftp" => 21,
2032 "gopher" => 70,
2033 "http" => 80,
2034 "https" => 443,
2035 "ws" => 80,
2036 "wss" => 443,
2037 _ => return None,
2038 })
2039}
2040
2041fn origin_scheme_part_match(a: &Origin, b: &str) -> MatchResult {
2042 if let Origin::Tuple(scheme, _host, _port) = a {
2043 scheme_part_match(&scheme[..], b)
2044 } else {
2045 DoesNotMatch
2046 }
2047}
2048
2049fn scheme_part_match(a: &str, b: &str) -> MatchResult {
2051 let a = a.to_ascii_lowercase();
2052 let b = b.to_ascii_lowercase();
2053 match (&a[..], &b[..]) {
2054 _ if a == b => Matches,
2055 ("http", "https") |
2056 ("ws", "wss") |
2057 ("wss", "https") => Matches,
2058 _ => DoesNotMatch,
2059 }
2060}
2061
2062#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2063pub enum HashAlgorithm {
2064 Sha256,
2065 Sha384,
2066 Sha512,
2067}
2068
2069impl HashAlgorithm {
2070 pub fn from_name(name: &str) -> Option<HashAlgorithm> {
2071 use HashAlgorithm::*;
2072 match name {
2073 "sha256" | "Sha256" | "sHa256" | "shA256" | "SHa256" | "ShA256" | "sHA256" | "SHA256" => Some(Sha256),
2074 "sha384" | "Sha384" | "sHa384" | "shA384" | "SHa384" | "ShA384" | "sHA384" | "SHA384" => Some(Sha384),
2075 "sha512" | "Sha512" | "sHa512" | "shA512" | "SHa512" | "ShA512" | "sHA512" | "SHA512" => Some(Sha512),
2076 _ => None,
2077 }
2078 }
2079 pub fn apply(self, value: &str) -> String {
2080 use base64::Engine as _;
2081 let bytes = value.as_bytes();
2082 let standard = base64::engine::general_purpose::STANDARD;
2083 match self {
2084 HashAlgorithm::Sha256 => standard.encode(sha2::Sha256::digest(bytes)),
2085 HashAlgorithm::Sha384 => standard.encode(sha2::Sha384::digest(bytes)),
2086 HashAlgorithm::Sha512 => standard.encode(sha2::Sha512::digest(bytes)),
2087 }
2088 }
2089}
2090
2091#[derive(Clone, Debug, Eq, PartialEq)]
2093pub struct HashFunction {
2094 algorithm: HashAlgorithm,
2095 value: String,
2096 }
2098
2099#[derive(Clone, Debug, Eq, PartialEq)]
2101pub enum SubresourceIntegrityMetadata {
2102 NoMetadata,
2103 IntegritySources(Vec<HashFunction>)
2104}
2105
2106static SUBRESOURCE_METADATA_GRAMMAR: Lazy<Regex> =
2109 Lazy::new(|| Regex::new(r#"(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)"#).unwrap());
2110
2111pub fn parse_subresource_integrity_metadata(string: &str) -> SubresourceIntegrityMetadata {
2113 let mut result = Vec::new();
2114 let mut empty = true;
2115 for token in split_ascii_whitespace(string) {
2116 empty = false;
2117 if let Some(captures) = SUBRESOURCE_METADATA_GRAMMAR.captures(token) {
2118 if let (Some(algorithm), Some(value)) = (captures.name("algorithm").and_then(|a| HashAlgorithm::from_name(a.as_str())), captures.name("value")) {
2119 result.push(HashFunction{ algorithm, value: String::from(value.as_str()) });
2120 }
2121 }
2122 }
2123 if empty {
2124 SubresourceIntegrityMetadata::NoMetadata
2125 } else {
2126 SubresourceIntegrityMetadata::IntegritySources(result)
2127 }
2128}
2129
2130#[cfg(test)]
2131mod test {
2132 use super::*;
2133 #[test]
2134 fn empty_directive_is_not_valid() {
2135 let d = Directive {
2136 name: String::new(),
2137 value: Vec::new(),
2138 };
2139 assert!(!d.is_valid());
2140 }
2141 #[test]
2142 pub fn duplicate_policy_is_not_valid() {
2143 let d = Directive {
2144 name: "test".to_owned(),
2145 value: vec!["test".to_owned()],
2146 };
2147 let p = Policy {
2148 directive_set: vec![d.clone(), d.clone()],
2149 disposition: PolicyDisposition::Enforce,
2150 source: PolicySource::Header,
2151 };
2152 assert!(!p.is_valid());
2153 }
2154 #[test]
2155 pub fn basic_policy_is_valid() {
2156 let p = Policy::parse("script-src notriddle.com", PolicySource::Header, PolicyDisposition::Enforce);
2157 assert!(p.is_valid());
2158 }
2159 #[test]
2160 pub fn policy_with_empty_directive_set_is_not_valid() {
2161 let p = Policy {
2162 directive_set: vec![],
2163 disposition: PolicyDisposition::Enforce,
2164 source: PolicySource::Header,
2165 };
2166 assert!(!p.is_valid());
2167 }
2168
2169 #[test]
2170 pub fn prefetch_request_does_not_violate_policy() {
2171 let request = Request {
2172 url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
2173 origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
2174 redirect_count: 0,
2175 destination: Destination::Script,
2176 initiator: Initiator::Prefetch,
2177 nonce: String::new(),
2178 integrity_metadata: String::new(),
2179 parser_metadata: ParserMetadata::None,
2180 };
2181
2182 let p = Policy::parse("child-src 'self'", PolicySource::Header, PolicyDisposition::Enforce);
2183
2184 let violation_result = p.does_request_violate_policy(&request);
2185
2186 assert!(violation_result == Violates::DoesNotViolate);
2187 }
2188
2189 #[test]
2190 pub fn prefetch_request_violates_policy() {
2191 let request = Request {
2192 url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
2193 origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
2194 redirect_count: 0,
2195 destination: Destination::ServiceWorker,
2196 initiator: Initiator::None,
2197 nonce: String::new(),
2198 integrity_metadata: String::new(),
2199 parser_metadata: ParserMetadata::None,
2200 };
2201
2202 let p = Policy::parse("default-src 'none'; script-src 'self' ", PolicySource::Header, PolicyDisposition::Enforce);
2203
2204 let violation_result = p.does_request_violate_policy(&request);
2205
2206 let expected_result = Violates::Directive(Directive { name: String::from("script-src"), value: vec![String::from("'self'")] });
2207
2208 assert!(violation_result == expected_result);
2209 }
2210
2211 #[test]
2212 pub fn prefetch_request_is_allowed_by_directive() {
2213 let request = Request {
2214 url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
2215 origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
2216 redirect_count: 0,
2217 destination: Destination::Script,
2218 initiator: Initiator::Prefetch,
2219 nonce: String::new(),
2220 integrity_metadata: String::new(),
2221 parser_metadata: ParserMetadata::None,
2222 };
2223
2224 let p = Policy::parse("default-src 'none'; child-src 'self'", PolicySource::Header, PolicyDisposition::Enforce);
2225
2226 let violation_result = p.does_request_violate_policy(&request);
2227
2228 assert!(violation_result == Violates::DoesNotViolate);
2229 }
2230
2231 #[test]
2232 pub fn trusted_type_policy_is_valid() {
2233 let p = Policy::parse("trusted-types 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2234 assert!(p.is_valid());
2235 assert_eq!(p.directive_set[0].value, vec!["'none'".to_owned()]);
2236 }
2237
2238 #[test]
2239 pub fn csp_list_is_valid() {
2240 let csp_list = CspList::parse("default-src 'none'; child-src 'self', trusted-types 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2241 assert!(csp_list.is_valid());
2242 assert_eq!(csp_list.0[1].directive_set[0].value, vec!["'none'".to_owned()]);
2243 }
2244
2245 #[test]
2246 pub fn no_trusted_types_specified_allows_all_policies() {
2247 let csp_list = CspList::parse("default-src 'none'; child-src 'self'", PolicySource::Meta, PolicyDisposition::Enforce);
2248 assert!(csp_list.is_valid());
2249 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2250 assert_eq!(check_result, CheckResult::Allowed);
2251 assert!(violations.is_empty());
2252 }
2253
2254 #[test]
2255 pub fn none_does_not_allow_for_any_policy() {
2256 let csp_list = CspList::parse("trusted-types 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2257 assert!(csp_list.is_valid());
2258 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2259 assert!(check_result == CheckResult::Blocked);
2260 assert_eq!(violations.len(), 1);
2261 }
2262
2263 #[test]
2264 pub fn extra_none_allows_all_policies() {
2265 let csp_list = CspList::parse("trusted-types some-policy 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2266 assert!(csp_list.is_valid());
2267 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2268 assert!(check_result == CheckResult::Allowed);
2269 assert!(violations.is_empty());
2270 }
2271
2272 #[test]
2273 pub fn explicit_policy_named_is_allowed() {
2274 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2275 assert!(csp_list.is_valid());
2276 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2277 assert_eq!(check_result, CheckResult::Allowed);
2278 assert!(violations.is_empty());
2279 }
2280
2281 #[test]
2282 pub fn other_policy_name_is_blocked() {
2283 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2284 assert!(csp_list.is_valid());
2285 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2286 assert!(check_result == CheckResult::Blocked);
2287 assert_eq!(violations.len(), 1);
2288 }
2289
2290 #[test]
2291 pub fn invalid_characters_in_policy_name_is_blocked() {
2292 let csp_list = CspList::parse("trusted-types My?Policy", PolicySource::Meta, PolicyDisposition::Enforce);
2293 assert!(csp_list.is_valid());
2294 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("My?Policy", &["My?Policy"]);
2295 assert!(check_result == CheckResult::Blocked);
2296 assert_eq!(violations.len(), 1);
2297 }
2298
2299 #[test]
2300 pub fn already_created_policy_is_blocked() {
2301 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2302 assert!(csp_list.is_valid());
2303 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2304 assert!(check_result == CheckResult::Blocked);
2305 assert_eq!(violations.len(), 1);
2306 }
2307
2308 #[test]
2309 pub fn already_created_policy_is_allowed_with_allow_duplicates() {
2310 let csp_list = CspList::parse("trusted-types MyPolicy 'allow-duplicates'", PolicySource::Meta, PolicyDisposition::Enforce);
2311 assert!(csp_list.is_valid());
2312 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2313 assert!(check_result == CheckResult::Allowed);
2314 assert!(violations.is_empty());
2315 }
2316
2317 #[test]
2318 pub fn only_report_policy_issues_for_disposition_report() {
2319 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Report);
2320 assert!(csp_list.is_valid());
2321 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2322 assert!(check_result == CheckResult::Allowed);
2323 assert_eq!(violations.len(), 1);
2324 }
2325
2326 #[test]
2327 pub fn wildcard_allows_all_policies() {
2328 let csp_list = CspList::parse("trusted-types *", PolicySource::Meta, PolicyDisposition::Report);
2329 assert!(csp_list.is_valid());
2330 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2331 assert!(check_result == CheckResult::Allowed);
2332 assert!(violations.is_empty());
2333 }
2334
2335 #[test]
2336 pub fn violation_has_correct_directive() {
2337 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2338 assert!(csp_list.is_valid());
2339 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2340 assert!(check_result == CheckResult::Blocked);
2341 assert_eq!(violations.len(), 1);
2342 assert_eq!(violations[0].directive, csp_list.0[0].directive_set[0]);
2343 }
2344
2345 #[test]
2346 pub fn long_policy_name_is_truncated() {
2347 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2348 assert!(csp_list.is_valid());
2349 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("SuperLongPolicyNameThatExceeds40Characters", &[]);
2350 assert!(check_result == CheckResult::Blocked);
2351 assert_eq!(violations.len(), 1);
2352 assert!(matches!(&violations[0].resource, ViolationResource::TrustedTypePolicy { sample } if sample == "SuperLongPolicyNameThatExceeds40Characte"));
2353 }
2354}