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, url_processor: TrustedTypesUrlProcessor) -> (CheckResult, Vec<Violation>)
646 where
647 TrustedTypesUrlProcessor: Fn(&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, &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, url_processor: &TrustedTypesUrlProcessor, _policy: &Policy) -> CheckResult
1404 where
1405 TrustedTypesUrlProcessor: Fn(&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..]).unwrap_or("");
1882 let url_port = url_port(url);
1883 if port_part_match(port_part, &url_port[..], url.scheme()) != Matches {
1884 return DoesNotMatch;
1885 }
1886 let path_part = captures.name("path").map(|path_part| path_part.as_str()).unwrap_or("");
1887 if path_part != "/" && redirect_count == 0 {
1888 let path = url.path();
1889 if path_part_match(path_part, path) != Matches {
1890 return DoesNotMatch;
1891 }
1892 }
1893 return Matches;
1894 }
1895 if ascii_case_insensitive_match(expression, "'self'") {
1896 if *origin == url.origin() {
1897 return Matches;
1898 }
1899 if let Origin::Tuple(scheme, host, port) = origin {
1900 let hosts_are_the_same = Some(host) == url.host().map(|p| p.to_owned()).as_ref();
1901 let ports_are_the_same = Some(*port) == url.port();
1902 let origins_port_is_default_for_scheme = Some(*port) == default_port(scheme);
1903 let url_port_is_default_port_for_scheme = url.port() == default_port(scheme)
1904 && default_port(scheme).is_some();
1905 let ports_are_default = url_port_is_default_port_for_scheme && origins_port_is_default_for_scheme;
1906 if hosts_are_the_same
1907 && (ports_are_the_same || ports_are_default)
1908 && ((url_scheme == "https" || url_scheme == "wss")
1909 || (scheme == "http" && (url_scheme == "http" || url_scheme == "ws"))) {
1910 return Matches;
1911 }
1912 }
1913 }
1914 DoesNotMatch
1915}
1916
1917fn host_part_match(pattern: &str, host: &str) -> MatchResult {
1919 debug_assert!(!host.is_empty());
1920 if host.is_empty() {
1922 return DoesNotMatch;
1923 }
1924 if pattern.as_bytes()[0] == b'*' {
1925 if pattern.len() == 1 {
1927 return Matches;
1928 }
1929 if pattern.as_bytes()[1] == b'.' {
1931 let remaining_pattern = &pattern[1..];
1933 if remaining_pattern.len() > host.len() {
1934 return DoesNotMatch;
1935 }
1936 let remaining_host = &host[(host.len()-remaining_pattern.len())..];
1937 debug_assert_eq!(remaining_host.len(), remaining_pattern.len());
1938 if ascii_case_insensitive_match(remaining_pattern, remaining_host) {
1940 return Matches;
1941 }
1942 return DoesNotMatch;
1944 }
1945 }
1946 if !ascii_case_insensitive_match(pattern, host) {
1948 return DoesNotMatch;
1949 }
1950 static IPV4_ADDRESS_RULE: Lazy<Regex> =
1951 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());
1952 if IPV4_ADDRESS_RULE.is_match(pattern) && pattern != "127.0.0.1" {
1953 return DoesNotMatch;
1954 }
1955 if pattern.as_bytes()[0] == b'[' {
1959 return DoesNotMatch;
1960 }
1961 Matches
1963}
1964
1965fn port_part_match(port_a: &str, port_b: &str, scheme_b: &str) -> MatchResult {
1967 if port_a.is_empty() {
1968 if port_b == default_port_str(scheme_b) {
1969 return Matches;
1970 } else {
1971 return DoesNotMatch;
1972 }
1973 }
1974 if port_a == "*" {
1975 return Matches;
1976 }
1977 if port_a == port_b {
1978 return Matches;
1979 }
1980 if port_b.is_empty() {
1981 if port_a == default_port_str(scheme_b) {
1982 return Matches;
1983 } else {
1984 return DoesNotMatch;
1985 }
1986 }
1987 DoesNotMatch
1988}
1989
1990fn path_part_match(path_a: &str, path_b: &str) -> MatchResult {
1992 if path_a.is_empty() {
1993 return Matches;
1994 }
1995 if path_a == "/" && path_b.is_empty() {
1996 return Matches;
1997 }
1998 let exact_match = path_a.as_bytes()[path_a.len()-1] != b'/';
1999 let (mut path_list_a, path_list_b): (Vec<&str>, Vec<&str>) =
2000 (path_a.split('/').collect(), path_b.split('/').collect());
2001 if path_list_a.len() > path_list_b.len() {
2002 return DoesNotMatch;
2003 }
2004 if exact_match && path_list_a.len() != path_list_b.len() {
2005 return DoesNotMatch;
2006 }
2007 if !exact_match {
2008 debug_assert_eq!(path_list_a[path_list_a.len()-1], "");
2009 path_list_a.pop();
2010 }
2011 let mut piece_b_iter = path_list_b.iter();
2012 for piece_a in &path_list_a {
2013 let piece_b = piece_b_iter.next().unwrap();
2014 let piece_a: Vec<u8> = percent_encoding::percent_decode(piece_a.as_bytes()).collect();
2015 let piece_b: Vec<u8> = percent_encoding::percent_decode(piece_b.as_bytes()).collect();
2016 if piece_a != piece_b {
2017 return DoesNotMatch;
2018 }
2019 }
2020 Matches
2021}
2022
2023fn url_port(url: &Url) -> String {
2024 match url.port() {
2025 Some(num) => num.to_string(),
2026 None => default_port_str(url.scheme()).to_string(),
2027 }
2028}
2029
2030fn default_port_str(scheme: &str) -> &'static str {
2031 match scheme {
2032 "ftp" => "21",
2033 "gopher" => "70",
2034 "http" => "80",
2035 "https" => "443",
2036 "ws" => "80",
2037 "wss" => "443",
2038 _ => "",
2039 }
2040}
2041
2042fn default_port(scheme: &str) -> Option<u16> {
2043 Some(match scheme {
2044 "ftp" => 21,
2045 "gopher" => 70,
2046 "http" => 80,
2047 "https" => 443,
2048 "ws" => 80,
2049 "wss" => 443,
2050 _ => return None,
2051 })
2052}
2053
2054fn origin_scheme_part_match(a: &Origin, b: &str) -> MatchResult {
2055 if let Origin::Tuple(scheme, _host, _port) = a {
2056 scheme_part_match(&scheme[..], b)
2057 } else {
2058 DoesNotMatch
2059 }
2060}
2061
2062fn scheme_part_match(a: &str, b: &str) -> MatchResult {
2064 let a = a.to_ascii_lowercase();
2065 let b = b.to_ascii_lowercase();
2066 match (&a[..], &b[..]) {
2067 _ if a == b => Matches,
2068 ("http", "https") |
2069 ("ws", "wss") |
2070 ("wss", "https") => Matches,
2071 _ => DoesNotMatch,
2072 }
2073}
2074
2075#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2076pub enum HashAlgorithm {
2077 Sha256,
2078 Sha384,
2079 Sha512,
2080}
2081
2082impl HashAlgorithm {
2083 pub fn from_name(name: &str) -> Option<HashAlgorithm> {
2084 use HashAlgorithm::*;
2085 match name {
2086 "sha256" | "Sha256" | "sHa256" | "shA256" | "SHa256" | "ShA256" | "sHA256" | "SHA256" => Some(Sha256),
2087 "sha384" | "Sha384" | "sHa384" | "shA384" | "SHa384" | "ShA384" | "sHA384" | "SHA384" => Some(Sha384),
2088 "sha512" | "Sha512" | "sHa512" | "shA512" | "SHa512" | "ShA512" | "sHA512" | "SHA512" => Some(Sha512),
2089 _ => None,
2090 }
2091 }
2092 pub fn apply(self, value: &str) -> String {
2093 use base64::Engine as _;
2094 let bytes = value.as_bytes();
2095 let standard = base64::engine::general_purpose::STANDARD;
2096 match self {
2097 HashAlgorithm::Sha256 => standard.encode(sha2::Sha256::digest(bytes)),
2098 HashAlgorithm::Sha384 => standard.encode(sha2::Sha384::digest(bytes)),
2099 HashAlgorithm::Sha512 => standard.encode(sha2::Sha512::digest(bytes)),
2100 }
2101 }
2102}
2103
2104#[derive(Clone, Debug, Eq, PartialEq)]
2106pub struct HashFunction {
2107 algorithm: HashAlgorithm,
2108 value: String,
2109 }
2111
2112#[derive(Clone, Debug, Eq, PartialEq)]
2114pub enum SubresourceIntegrityMetadata {
2115 NoMetadata,
2116 IntegritySources(Vec<HashFunction>)
2117}
2118
2119static SUBRESOURCE_METADATA_GRAMMAR: Lazy<Regex> =
2122 Lazy::new(|| Regex::new(r#"(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)"#).unwrap());
2123
2124pub fn parse_subresource_integrity_metadata(string: &str) -> SubresourceIntegrityMetadata {
2126 let mut result = Vec::new();
2127 let mut empty = true;
2128 for token in split_ascii_whitespace(string) {
2129 empty = false;
2130 if let Some(captures) = SUBRESOURCE_METADATA_GRAMMAR.captures(token) {
2131 if let (Some(algorithm), Some(value)) = (captures.name("algorithm").and_then(|a| HashAlgorithm::from_name(a.as_str())), captures.name("value")) {
2132 result.push(HashFunction{ algorithm, value: String::from(value.as_str()) });
2133 }
2134 }
2135 }
2136 if empty {
2137 SubresourceIntegrityMetadata::NoMetadata
2138 } else {
2139 SubresourceIntegrityMetadata::IntegritySources(result)
2140 }
2141}
2142
2143#[cfg(test)]
2144mod test {
2145 use super::*;
2146 #[test]
2147 fn empty_directive_is_not_valid() {
2148 let d = Directive {
2149 name: String::new(),
2150 value: Vec::new(),
2151 };
2152 assert!(!d.is_valid());
2153 }
2154 #[test]
2155 pub fn duplicate_policy_is_not_valid() {
2156 let d = Directive {
2157 name: "test".to_owned(),
2158 value: vec!["test".to_owned()],
2159 };
2160 let p = Policy {
2161 directive_set: vec![d.clone(), d.clone()],
2162 disposition: PolicyDisposition::Enforce,
2163 source: PolicySource::Header,
2164 };
2165 assert!(!p.is_valid());
2166 }
2167 #[test]
2168 pub fn basic_policy_is_valid() {
2169 let p = Policy::parse("script-src notriddle.com", PolicySource::Header, PolicyDisposition::Enforce);
2170 assert!(p.is_valid());
2171 }
2172 #[test]
2173 pub fn policy_with_empty_directive_set_is_not_valid() {
2174 let p = Policy {
2175 directive_set: vec![],
2176 disposition: PolicyDisposition::Enforce,
2177 source: PolicySource::Header,
2178 };
2179 assert!(!p.is_valid());
2180 }
2181
2182 #[test]
2183 pub fn prefetch_request_does_not_violate_policy() {
2184 let request = Request {
2185 url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
2186 origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
2187 redirect_count: 0,
2188 destination: Destination::Script,
2189 initiator: Initiator::Prefetch,
2190 nonce: String::new(),
2191 integrity_metadata: String::new(),
2192 parser_metadata: ParserMetadata::None,
2193 };
2194
2195 let p = Policy::parse("child-src 'self'", PolicySource::Header, PolicyDisposition::Enforce);
2196
2197 let violation_result = p.does_request_violate_policy(&request);
2198
2199 assert!(violation_result == Violates::DoesNotViolate);
2200 }
2201
2202 #[test]
2203 pub fn prefetch_request_violates_policy() {
2204 let request = Request {
2205 url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
2206 origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
2207 redirect_count: 0,
2208 destination: Destination::ServiceWorker,
2209 initiator: Initiator::None,
2210 nonce: String::new(),
2211 integrity_metadata: String::new(),
2212 parser_metadata: ParserMetadata::None,
2213 };
2214
2215 let p = Policy::parse("default-src 'none'; script-src 'self' ", PolicySource::Header, PolicyDisposition::Enforce);
2216
2217 let violation_result = p.does_request_violate_policy(&request);
2218
2219 let expected_result = Violates::Directive(Directive { name: String::from("script-src"), value: vec![String::from("'self'")] });
2220
2221 assert!(violation_result == expected_result);
2222 }
2223
2224 #[test]
2225 pub fn prefetch_request_is_allowed_by_directive() {
2226 let request = Request {
2227 url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
2228 origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
2229 redirect_count: 0,
2230 destination: Destination::Script,
2231 initiator: Initiator::Prefetch,
2232 nonce: String::new(),
2233 integrity_metadata: String::new(),
2234 parser_metadata: ParserMetadata::None,
2235 };
2236
2237 let p = Policy::parse("default-src 'none'; child-src 'self'", PolicySource::Header, PolicyDisposition::Enforce);
2238
2239 let violation_result = p.does_request_violate_policy(&request);
2240
2241 assert!(violation_result == Violates::DoesNotViolate);
2242 }
2243
2244 #[test]
2245 pub fn trusted_type_policy_is_valid() {
2246 let p = Policy::parse("trusted-types 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2247 assert!(p.is_valid());
2248 assert_eq!(p.directive_set[0].value, vec!["'none'".to_owned()]);
2249 }
2250
2251 #[test]
2252 pub fn csp_list_is_valid() {
2253 let csp_list = CspList::parse("default-src 'none'; child-src 'self', trusted-types 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2254 assert!(csp_list.is_valid());
2255 assert_eq!(csp_list.0[1].directive_set[0].value, vec!["'none'".to_owned()]);
2256 }
2257
2258 #[test]
2259 pub fn no_trusted_types_specified_allows_all_policies() {
2260 let csp_list = CspList::parse("default-src 'none'; child-src 'self'", PolicySource::Meta, PolicyDisposition::Enforce);
2261 assert!(csp_list.is_valid());
2262 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec![]);
2263 assert_eq!(check_result, CheckResult::Allowed);
2264 assert!(violations.is_empty());
2265 }
2266
2267 #[test]
2268 pub fn none_does_not_allow_for_any_policy() {
2269 let csp_list = CspList::parse("trusted-types 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2270 assert!(csp_list.is_valid());
2271 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("some-policy", &vec![]);
2272 assert!(check_result == CheckResult::Blocked);
2273 assert_eq!(violations.len(), 1);
2274 }
2275
2276 #[test]
2277 pub fn extra_none_allows_all_policies() {
2278 let csp_list = CspList::parse("trusted-types some-policy 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2279 assert!(csp_list.is_valid());
2280 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("some-policy", &vec![]);
2281 assert!(check_result == CheckResult::Allowed);
2282 assert!(violations.is_empty());
2283 }
2284
2285 #[test]
2286 pub fn explicit_policy_named_is_allowed() {
2287 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2288 assert!(csp_list.is_valid());
2289 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec![]);
2290 assert_eq!(check_result, CheckResult::Allowed);
2291 assert!(violations.is_empty());
2292 }
2293
2294 #[test]
2295 pub fn other_policy_name_is_blocked() {
2296 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2297 assert!(csp_list.is_valid());
2298 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &vec![]);
2299 assert!(check_result == CheckResult::Blocked);
2300 assert_eq!(violations.len(), 1);
2301 }
2302
2303 #[test]
2304 pub fn invalid_characters_in_policy_name_is_blocked() {
2305 let csp_list = CspList::parse("trusted-types My?Policy", PolicySource::Meta, PolicyDisposition::Enforce);
2306 assert!(csp_list.is_valid());
2307 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("My?Policy", &vec!["My?Policy"]);
2308 assert!(check_result == CheckResult::Blocked);
2309 assert_eq!(violations.len(), 1);
2310 }
2311
2312 #[test]
2313 pub fn already_created_policy_is_blocked() {
2314 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2315 assert!(csp_list.is_valid());
2316 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec!["MyPolicy"]);
2317 assert!(check_result == CheckResult::Blocked);
2318 assert_eq!(violations.len(), 1);
2319 }
2320
2321 #[test]
2322 pub fn already_created_policy_is_allowed_with_allow_duplicates() {
2323 let csp_list = CspList::parse("trusted-types MyPolicy 'allow-duplicates'", PolicySource::Meta, PolicyDisposition::Enforce);
2324 assert!(csp_list.is_valid());
2325 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec!["MyPolicy"]);
2326 assert!(check_result == CheckResult::Allowed);
2327 assert!(violations.is_empty());
2328 }
2329
2330 #[test]
2331 pub fn only_report_policy_issues_for_disposition_report() {
2332 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Report);
2333 assert!(csp_list.is_valid());
2334 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec!["MyPolicy"]);
2335 assert!(check_result == CheckResult::Allowed);
2336 assert_eq!(violations.len(), 1);
2337 }
2338
2339 #[test]
2340 pub fn wildcard_allows_all_policies() {
2341 let csp_list = CspList::parse("trusted-types *", PolicySource::Meta, PolicyDisposition::Report);
2342 assert!(csp_list.is_valid());
2343 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec![]);
2344 assert!(check_result == CheckResult::Allowed);
2345 assert!(violations.is_empty());
2346 }
2347
2348 #[test]
2349 pub fn violation_has_correct_directive() {
2350 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2351 assert!(csp_list.is_valid());
2352 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &vec![]);
2353 assert!(check_result == CheckResult::Blocked);
2354 assert_eq!(violations.len(), 1);
2355 assert_eq!(violations[0].directive, csp_list.0[0].directive_set[0]);
2356 }
2357
2358 #[test]
2359 pub fn long_policy_name_is_truncated() {
2360 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2361 assert!(csp_list.is_valid());
2362 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("SuperLongPolicyNameThatExceeds40Characters", &vec![]);
2363 assert!(check_result == CheckResult::Blocked);
2364 assert_eq!(violations.len(), 1);
2365 assert!(matches!(&violations[0].resource, ViolationResource::TrustedTypePolicy { sample } if sample == "SuperLongPolicyNameThatExceeds40Characte"));
2366 }
2367}