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 pub fn is_trusted_type_policy_creation_allowed(&self, policy_name: &str, created_policy_names: &[&str]) -> (CheckResult, Vec<Violation>) {
369 use CheckResult::*;
370 let mut result = Allowed;
372 let mut violations = Vec::new();
373 for policy in &self.0 {
375 let mut create_violation = false;
377 let directive = policy.directive_set.iter().find(|directive| directive.name == "trusted-types");
379 if let Some(directive) = directive {
381 if directive.value.len() == 1 && directive.value.contains(&"'none'".to_string()) {
383 create_violation = true;
384 }
385 if created_policy_names.contains(&policy_name) && !directive.value.iter().any(|v| v == "'allow-duplicates'") {
388 create_violation = true;
389 }
390 if !(TRUSTED_POLICY_SOURCE_GRAMMAR.is_match(&policy_name) && (directive.value.iter().any(|p| p == policy_name) || directive.value.iter().any(|v| v == "*"))) {
393 create_violation = true;
394 }
395 if !create_violation {
397 continue;
398 }
399 let max_length = cmp::min(40, policy_name.len());
400 let sample = policy_name[0..max_length].to_owned();
402 let violation = Violation {
405 directive: directive.clone(),
406 resource: ViolationResource::TrustedTypePolicy {
408 sample,
410 },
411 policy: policy.clone(),
412 };
413 violations.push(violation);
415 if policy.disposition == PolicyDisposition::Enforce {
417 result = Blocked
418 }
419 }
420 }
421 return (result, violations);
422 }
423 pub fn does_sink_type_require_trusted_types(&self, sink_group: &str, include_report_only_policies: bool) -> bool {
430 let sink_group = &sink_group.to_owned();
431 for policy in &self.0 {
433 let directive = policy.directive_set.iter().find(|directive| directive.name == "require-trusted-types-for");
435 if let Some(directive) = directive {
437 if !directive.value.contains(sink_group) {
439 continue;
440 }
441 let enforced = policy.disposition == PolicyDisposition::Enforce;
443 if enforced {
445 return true;
446 }
447 if include_report_only_policies {
449 return true;
450 }
451 }
452 }
453 false
455 }
456 pub fn should_sink_type_mismatch_violation_be_blocked_by_csp(&self, sink: &str, sink_group: &str, source: &str) -> (CheckResult, Vec<Violation>) {
463 use CheckResult::*;
464 let sink_group = &sink_group.to_owned();
465 let mut result = Allowed;
467 let mut violations = Vec::new();
468 let mut sample = source;
470 if sink == "Function" {
472 if sample.starts_with("function anonymous") {
474 sample = &sample[18..];
475 } else if sample.starts_with("async function anonymous") {
477 sample = &sample[24..];
478 } else if sample.starts_with("function* anonymous") {
480 sample = &sample[19..];
481 } else if sample.starts_with("async function* anonymous") {
483 sample = &sample[25..];
484 }
485 }
486 for policy in &self.0 {
488 let directive = policy.directive_set.iter().find(|directive| directive.name == "require-trusted-types-for");
490 let Some(directive) = directive else { continue };
492 if !directive.value.contains(sink_group) {
494 continue;
495 }
496 let max_length = cmp::min(40, sample.len());
498 let trimmed_sample = sample[0..max_length].to_owned();
499 violations.push(Violation {
502 resource: ViolationResource::TrustedTypeSink {
504 sample: sink.to_owned() + "|" + &trimmed_sample,
506 },
507 directive: directive.clone(),
508 policy: policy.clone(),
509 });
510 if policy.disposition == PolicyDisposition::Enforce {
512 result = Blocked
513 }
514 }
515 (result, violations)
517 }
518 pub fn get_sandboxing_flag_set_for_document(&self) -> Option<SandboxingFlagSet> {
520 self.0
523 .iter()
524 .flat_map(|policy| {
525 policy.directive_set
526 .iter()
527 .rev()
529 .find(|directive| directive.name == "sandbox")
532 .and_then(|directive| directive.get_sandboxing_flag_set_for_document(policy))
533 })
534 .next()
536 }
537 pub fn is_js_evaluation_allowed(&self, source: &str) -> (CheckResult, Vec<Violation>) {
539 let mut result = CheckResult::Allowed;
540 let mut violations = Vec::new();
541 for policy in &self.0 {
543 let directive = policy.directive_set
545 .iter()
546 .find(|directive| directive.name == "script-src")
549 .or_else(|| policy.directive_set.iter().find(|directive| directive.name == "default-src"));
552 let Some(directive) = directive else { continue };
554 let source_list = SourceList(&directive.value);
555 if source_list.does_a_source_list_allow_js_evaluation() == AllowResult::Allows {
556 continue;
557 }
558 let trusted_types_required = self.does_sink_type_require_trusted_types("'script'", false);
561 if trusted_types_required && directive.value.iter().any(|t| ascii_case_insensitive_match(&t[..], "'trusted-types-eval'")) {
564 continue;
565 }
566 if directive.value.iter().any(|t| ascii_case_insensitive_match(&t[..], "'unsafe-eval'")) {
569 continue;
570 }
571 let sample = if directive.value.iter().any(|t| &t[..] == "'report-sample'") {
574 let max_length = cmp::min(40, source.len());
575 Some(source[0..max_length].to_owned())
576 } else {
577 None
578 };
579 violations.push(Violation {
582 resource: ViolationResource::Eval { sample },
584 directive: directive.clone(),
585 policy: policy.clone(),
586 });
587 if policy.disposition == PolicyDisposition::Enforce {
589 result = CheckResult::Blocked
590 }
591 }
592 (result, violations)
593 }
594 pub fn is_wasm_evaluation_allowed(&self) -> (CheckResult, Vec<Violation>) {
596 let mut result = CheckResult::Allowed;
597 let mut violations = Vec::new();
598 for policy in &self.0 {
600 let directive = policy.directive_set
602 .iter()
603 .find(|directive| directive.name == "script-src")
606 .or_else(|| policy.directive_set.iter().find(|directive| directive.name == "default-src"));
609 let Some(directive) = directive else { continue };
610 let source_list = SourceList(&directive.value);
611 if source_list.does_a_source_list_allow_wasm_evaluation() == AllowResult::Allows {
616 continue;
617 }
618 violations.push(Violation {
621 resource: ViolationResource::WasmEval,
623 directive: directive.clone(),
624 policy: policy.clone(),
625 });
626 if policy.disposition == PolicyDisposition::Enforce {
628 result = CheckResult::Blocked
629 }
630 }
631 (result, violations)
632 }
633 pub fn should_navigation_request_be_blocked<TrustedTypesUrlProcessor>(
644 &self, request: &mut Request, navigation_check_type: NavigationCheckType, url_processor: TrustedTypesUrlProcessor) -> (CheckResult, Vec<Violation>)
645 where
646 TrustedTypesUrlProcessor: Fn(&str) -> Option<String>
647 {
648 let mut result = CheckResult::Allowed;
650 let mut violations = Vec::new();
651 for policy in &self.0 {
653 for directive in &policy.directive_set {
655 if directive.pre_navigation_check(request, navigation_check_type, &url_processor, policy) == CheckResult::Allowed {
658 continue;
659 }
660 violations.push(Violation {
664 resource: ViolationResource::Url(request.url.clone()),
666 directive: Directive {
667 name: get_the_effective_directive_for_request(request).to_owned(),
668 value: directive.value.clone(),
669 },
670 policy: policy.clone(),
671 });
672 if policy.disposition == PolicyDisposition::Enforce {
674 result = CheckResult::Blocked;
675 }
676 }
677 }
678 if result == CheckResult::Allowed && request.url.scheme() == "javascript" {
680 for policy in &self.0 {
682 for directive in &policy.directive_set {
684 if directive.inline_check(&Element { nonce: None }, InlineCheckType::Navigation, policy, request.url.as_str()) == CheckResult::Allowed {
687 continue;
688 }
689 violations.push(Violation {
693 resource: ViolationResource::Inline { sample: None },
695 directive: Directive {
696 name: get_the_effective_directive_for_inline_checks(InlineCheckType::Navigation).to_owned(),
699 value: directive.value.clone(),
700 },
701 policy: policy.clone(),
702 });
703 if policy.disposition == PolicyDisposition::Enforce {
705 result = CheckResult::Blocked;
706 }
707 }
708 }
709 }
710 (result, violations)
711 }
712}
713
714#[derive(Clone, Debug)]
715pub struct Element<'a> {
716 pub nonce: Option<Cow<'a, str>>,
722}
723
724#[derive(Clone, Copy, Debug, Eq, PartialEq)]
730pub enum InlineCheckType {
731 Script,
732 ScriptAttribute,
733 Style,
734 StyleAttribute,
735 Navigation,
736}
737
738#[derive(Clone, Copy, Debug, Eq, PartialEq)]
744pub enum NavigationCheckType {
745 FormSubmission,
746 Other,
747}
748
749#[derive(Clone, Debug)]
755pub struct Request {
756 pub url: Url,
757 pub origin: Origin,
758 pub redirect_count: u32,
759 pub destination: Destination,
760 pub initiator: Initiator,
761 pub nonce: String,
762 pub integrity_metadata: String,
763 pub parser_metadata: ParserMetadata,
764}
765
766#[derive(Clone, Copy, Debug, Eq, PartialEq)]
767pub enum ParserMetadata {
768 ParserInserted,
769 NotParserInserted,
770 None,
771}
772
773#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
774#[derive(Clone, Copy, Debug, Eq, PartialEq)]
775pub enum Initiator {
776 Download,
777 ImageSet,
778 Manifest,
779 Prefetch,
780 Prerender,
781 Fetch,
782 Xslt,
783 None,
784}
785
786#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
787#[derive(Clone, Copy, Debug, Eq, PartialEq)]
788pub enum Destination {
789 None,
790 Audio,
791 AudioWorklet,
792 Document,
793 Embed,
794 Font,
795 Frame,
796 IFrame,
797 Image,
798 Json,
799 Manifest,
800 Object,
801 PaintWorklet,
802 Report,
803 Script,
804 ServiceWorker,
805 SharedWorker,
806 Style,
807 Track,
808 Video,
809 WebIdentity,
810 Worker,
811 Xslt,
812}
813
814pub struct InvalidDestination;
815
816impl FromStr for Destination {
817 type Err = InvalidDestination;
818
819 fn from_str(s: &str) -> Result<Self, Self::Err> {
820 let destination = match s {
821 "" => Self::None,
822 "audio" => Self::Audio,
823 "audioworklet" => Self::AudioWorklet,
824 "document" => Self::Document,
825 "embed" => Self::Embed,
826 "font" => Self::Font,
827 "frame" => Self::Frame,
828 "iframe" => Self::IFrame,
829 "image" => Self::Image,
830 "json" => Self::Json,
831 "manifest" => Self::Manifest,
832 "object" => Self::Object,
833 "paintworklet" => Self::PaintWorklet,
834 "report" => Self::Report,
835 "script" => Self::Script,
836 "serviceworker" => Self::ServiceWorker,
837 "sharedworker" => Self::SharedWorker,
838 "style" => Self::Style,
839 "track" => Self::Track,
840 "video" => Self::Video,
841 "webidentity" => Self::WebIdentity,
842 "worker" => Self::Worker,
843 "xslt" => Self::Xslt,
844 _ => return Err(InvalidDestination),
845 };
846
847 Ok(destination)
848 }
849}
850
851impl Destination {
852 pub fn is_script_like(self) -> bool {
854 use Destination::*;
855 matches!(self, AudioWorklet | PaintWorklet | Script | ServiceWorker | SharedWorker | Worker | Xslt)
856 }
857
858 pub const fn as_str(&self) -> &'static str {
859 match self {
860 Self::None => "",
861 Self::Audio => "audio",
862 Self::AudioWorklet => "audioworklet",
863 Self::Document => "document",
864 Self::Embed => "embed",
865 Self::Font => "font",
866 Self::Frame => "frame",
867 Self::IFrame => "iframe",
868 Self::Image => "image",
869 Self::Json => "json",
870 Self::Manifest => "manifest",
871 Self::Object => "object",
872 Self::PaintWorklet => "paintworklet",
873 Self::Report => "report",
874 Self::Script => "script",
875 Self::ServiceWorker => "serviceworker",
876 Self::SharedWorker => "sharedworker",
877 Self::Style => "style",
878 Self::Track => "track",
879 Self::Video => "video",
880 Self::WebIdentity => "webidentity",
881 Self::Worker => "worker",
882 Self::Xslt => "xslt",
883 }
884 }
885}
886
887#[derive(Clone, Debug)]
892pub struct Response {
893 pub url: Url,
894 pub redirect_count: u32,
895}
896
897#[derive(Clone, Debug)]
903#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
904pub struct Violation {
905 pub resource: ViolationResource,
906 pub directive: Directive,
907 pub policy: Policy,
908}
909
910#[derive(Clone, Debug)]
916#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
917pub enum ViolationResource {
918 Url(Url),
919 Inline {
920 sample: Option<String>,
921 },
922 TrustedTypePolicy {
923 sample: String,
924 },
925 TrustedTypeSink {
926 sample: String,
927 },
928 Eval {
929 sample: Option<String>,
930 },
931 WasmEval,
932}
933
934#[derive(Clone, Debug, Eq, PartialEq)]
939#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
940pub enum CheckResult {
941 Allowed,
942 Blocked,
943}
944
945#[derive(Clone, Debug, Eq, PartialEq)]
949#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
950pub enum Violates {
951 DoesNotViolate,
952 Directive(Directive),
953}
954
955#[derive(Clone, Copy, Debug, Eq, PartialEq)]
957#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
958pub enum PolicyDisposition {
959 Enforce,
960 Report,
961}
962
963#[derive(Clone, Copy, Debug, Eq, PartialEq)]
965#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
966pub enum PolicySource {
967 Header,
968 Meta,
969}
970
971#[derive(Clone, Debug, Eq, PartialEq)]
973#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
974pub struct Directive {
975 pub name: String,
976 pub value: Vec<String>,
977}
978
979impl Display for Directive {
980 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
981 <str as Display>::fmt(&self.name[..], f)?;
982 write!(f, " ")?;
983 for (i, token) in self.value.iter().enumerate() {
984 if i != 0 {
985 write!(f, " ")?;
986 }
987 <str as Display>::fmt(&token[..], f)?;
988 }
989 Ok(())
990 }
991}
992
993impl Directive {
994 pub fn is_valid(&self) -> bool {
996 DIRECTIVE_NAME_GRAMMAR.is_match(&self.name) &&
997 self.value.iter().all(|t| DIRECTIVE_VALUE_TOKEN_GRAMMAR.is_match(&t[..]))
998 }
999 pub fn pre_request_check(&self, request: &Request, policy: &Policy) -> CheckResult {
1001 use CheckResult::*;
1002 match &self.name[..] {
1003 "child-src" => {
1004 let name = get_the_effective_directive_for_request(request);
1005 if !should_fetch_directive_execute(name, "child-src", policy) {
1006 return Allowed;
1007 }
1008 (Directive {
1009 name: String::from(name),
1010 value: self.value.clone(),
1011 }).pre_request_check(request, policy)
1012 },
1013 "connect-src" => {
1014 let name = get_the_effective_directive_for_request(request);
1015 if !should_fetch_directive_execute(name, "connect-src", policy) {
1016 return Allowed;
1017 }
1018 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1019 return Blocked;
1020 }
1021 Allowed
1022 }
1023 "default-src" => {
1024 let name = get_the_effective_directive_for_request(request);
1025 if !should_fetch_directive_execute(name, "default-src", policy) {
1026 return Allowed;
1027 }
1028 (Directive {
1029 name: String::from(name),
1030 value: self.value.clone(),
1031 }).pre_request_check(request, policy)
1032 }
1033 "font-src" => {
1034 let name = get_the_effective_directive_for_request(request);
1035 if !should_fetch_directive_execute(name, "font-src", policy) {
1036 return Allowed;
1037 }
1038 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1039 return Blocked;
1040 }
1041 Allowed
1042 }
1043 "frame-src" => {
1044 let name = get_the_effective_directive_for_request(request);
1045 if !should_fetch_directive_execute(name, "frame-src", policy) {
1046 return Allowed;
1047 }
1048 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1049 return Blocked;
1050 }
1051 Allowed
1052 }
1053 "img-src" => {
1054 let name = get_the_effective_directive_for_request(request);
1055 if !should_fetch_directive_execute(name, "img-src", policy) {
1056 return Allowed;
1057 }
1058 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1059 return Blocked;
1060 }
1061 Allowed
1062 }
1063 "manifest-src" => {
1064 let name = get_the_effective_directive_for_request(request);
1065 if !should_fetch_directive_execute(name, "manifest-src", policy) {
1066 return Allowed;
1067 }
1068 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1069 return Blocked;
1070 }
1071 Allowed
1072 }
1073 "media-src" => {
1074 let name = get_the_effective_directive_for_request(request);
1075 if !should_fetch_directive_execute(name, "media-src", policy) {
1076 return Allowed;
1077 }
1078 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1079 return Blocked;
1080 }
1081 Allowed
1082 }
1083 "object-src" => {
1084 let name = get_the_effective_directive_for_request(request);
1085 if !should_fetch_directive_execute(name, "object-src", policy) {
1086 return Allowed;
1087 }
1088 if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1089 return Blocked;
1090 }
1091 Allowed
1092 }
1093 "script-src" => {
1094 let name = get_the_effective_directive_for_request(request);
1095 if !should_fetch_directive_execute(name, "script-src", policy) {
1096 return Allowed;
1097 }
1098 script_directives_prerequest_check(request, self)
1099 }
1100 "script-src-elem" => {
1101 let name = get_the_effective_directive_for_request(request);
1102 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1103 return Allowed;
1104 }
1105 script_directives_prerequest_check(request, self)
1106 }
1107 "style-src" => {
1108 let name = get_the_effective_directive_for_request(request);
1109 if !should_fetch_directive_execute(name, "style-src", policy) {
1110 return Allowed;
1111 }
1112 let source_list = SourceList(&self.value);
1113 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1114 return Allowed;
1115 }
1116 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1117 return Blocked;
1118 }
1119 Allowed
1120 }
1121 "style-src-elem" => {
1122 let name = get_the_effective_directive_for_request(request);
1123 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1124 return Allowed;
1125 }
1126 let source_list = SourceList(&self.value);
1127 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1128 return Allowed;
1129 }
1130 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1131 return Blocked;
1132 }
1133 Allowed
1134 }
1135 "worker-src" => {
1136 let name = get_the_effective_directive_for_request(request);
1137 if !should_fetch_directive_execute(name, "worker-src", policy) {
1138 return Allowed;
1139 }
1140 let source_list = SourceList(&self.value);
1141 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1142 return Blocked;
1143 }
1144 Allowed
1145 }
1146 _ => Allowed,
1147 }
1148 }
1149 pub fn post_request_check(&self, request: &Request, response: &Response, policy: &Policy) -> CheckResult {
1151 use CheckResult::*;
1152 match &self.name[..] {
1153 "child-src" => {
1154 let name = get_the_effective_directive_for_request(request);
1155 if !should_fetch_directive_execute(name, "child-src", policy) {
1156 return Allowed;
1157 }
1158 Directive {
1159 name: name.to_owned(),
1160 value: self.value.clone()
1161 }.post_request_check(request, response, policy)
1162 }
1163 "connect-src" => {
1164 let name = get_the_effective_directive_for_request(request);
1165 if !should_fetch_directive_execute(name, "connect-src", policy) {
1166 return Allowed;
1167 }
1168 let source_list = SourceList(&self.value);
1169 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1170 return Blocked;
1171 }
1172 Allowed
1173 }
1174 "default-src" => {
1175 let name = get_the_effective_directive_for_request(request);
1176 if !should_fetch_directive_execute(name, "default-src", policy) {
1177 return Allowed;
1178 }
1179 Directive {
1180 name: name.to_owned(),
1181 value: self.value.clone(),
1182 }.post_request_check(request, response, policy)
1183 }
1184 "font-src" => {
1185 let name = get_the_effective_directive_for_request(request);
1186 if !should_fetch_directive_execute(name, "font-src", policy) {
1187 return Allowed;
1188 }
1189 let source_list = SourceList(&self.value);
1190 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1191 return Blocked;
1192 }
1193 Allowed
1194 }
1195 "frame-src" => {
1196 let name = get_the_effective_directive_for_request(request);
1197 if !should_fetch_directive_execute(name, "frame-src", policy) {
1198 return Allowed;
1199 }
1200 let source_list = SourceList(&self.value);
1201 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1202 return Blocked;
1203 }
1204 Allowed
1205 }
1206 "img-src" => {
1207 let name = get_the_effective_directive_for_request(request);
1208 if !should_fetch_directive_execute(name, "img-src", policy) {
1209 return Allowed;
1210 }
1211 let source_list = SourceList(&self.value);
1212 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1213 return Blocked;
1214 }
1215 Allowed
1216 }
1217 "manifest-src" => {
1218 let name = get_the_effective_directive_for_request(request);
1219 if !should_fetch_directive_execute(name, "manifest-src", policy) {
1220 return Allowed;
1221 }
1222 let source_list = SourceList(&self.value);
1223 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1224 return Blocked;
1225 }
1226 Allowed
1227 }
1228 "media-src" => {
1229 let name = get_the_effective_directive_for_request(request);
1230 if !should_fetch_directive_execute(name, "media-src", policy) {
1231 return Allowed;
1232 }
1233 let source_list = SourceList(&self.value);
1234 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1235 return Blocked;
1236 }
1237 Allowed
1238 }
1239 "object-src" => {
1240 let name = get_the_effective_directive_for_request(request);
1241 if !should_fetch_directive_execute(name, "object-src", policy) {
1242 return Allowed;
1243 }
1244 let source_list = SourceList(&self.value);
1245 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1246 return Blocked;
1247 }
1248 Allowed
1249 }
1250 "script-src" => {
1251 let name = get_the_effective_directive_for_request(request);
1252 if !should_fetch_directive_execute(name, "script-src", policy) {
1253 return Allowed;
1254 }
1255 script_directives_postrequest_check(request, response, self)
1256 }
1257 "script-src-elem" => {
1258 let name = get_the_effective_directive_for_request(request);
1259 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1260 return Allowed;
1261 }
1262 script_directives_postrequest_check(request, response, self)
1263 }
1264 "style-src" => {
1265 let name = get_the_effective_directive_for_request(request);
1266 if !should_fetch_directive_execute(name, "style-src", policy) {
1267 return Allowed;
1268 }
1269 let source_list = SourceList(&self.value);
1270 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1271 return Allowed;
1272 }
1273 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1274 return Blocked;
1275 }
1276 Allowed
1277 }
1278 "style-src-elem" => {
1279 let name = get_the_effective_directive_for_request(request);
1280 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1281 return Allowed;
1282 }
1283 let source_list = SourceList(&self.value);
1284 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1285 return Allowed;
1286 }
1287 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1288 return Blocked;
1289 }
1290 Allowed
1291 }
1292 "worker-src" => {
1293 let name = get_the_effective_directive_for_request(request);
1294 if !should_fetch_directive_execute(name, "worker-src", policy) {
1295 return Allowed;
1296 }
1297 let source_list = SourceList(&self.value);
1298 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1299 return Blocked;
1300 }
1301 Allowed
1302 }
1303 _ => Allowed,
1304 }
1305 }
1306 pub fn inline_check(&self, element: &Element, type_: InlineCheckType, policy: &Policy, source: &str) -> CheckResult {
1308 use CheckResult::*;
1309 match &self.name[..] {
1310 "default-src" => {
1311 let name = get_the_effective_directive_for_inline_checks(type_);
1312 if !should_fetch_directive_execute(name, "default-src", policy) {
1313 return Allowed;
1314 }
1315 Directive {
1316 name: name.to_owned(),
1317 value: self.value.clone()
1318 }.inline_check(element, type_, policy, source)
1319 }
1320 "script-src" => {
1321 let name = get_the_effective_directive_for_inline_checks(type_);
1322 if !should_fetch_directive_execute(name, "script-src", policy) {
1323 return Allowed;
1324 }
1325 let source_list = SourceList(&self.value);
1326 if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1327 return Blocked;
1328 }
1329 Allowed
1330 }
1331 "script-src-elem" => {
1332 let name = get_the_effective_directive_for_inline_checks(type_);
1333 if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1334 return Allowed;
1335 }
1336 let source_list = SourceList(&self.value);
1337 if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1338 return Blocked;
1339 }
1340 Allowed
1341 }
1342 "script-src-attr" => {
1343 let name = get_the_effective_directive_for_inline_checks(type_);
1344 if !should_fetch_directive_execute(name, "script-src-attr", policy) {
1345 return Allowed;
1346 }
1347 let source_list = SourceList(&self.value);
1348 if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1349 return Blocked;
1350 }
1351 Allowed
1352 }
1353 "style-src" => {
1354 let name = get_the_effective_directive_for_inline_checks(type_);
1355 if !should_fetch_directive_execute(name, "style-src", policy) {
1356 return Allowed;
1357 }
1358 let source_list = SourceList(&self.value);
1359 if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1360 return Blocked;
1361 }
1362 Allowed
1363 }
1364 "style-src-elem" => {
1365 let name = get_the_effective_directive_for_inline_checks(type_);
1366 if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1367 return Allowed;
1368 }
1369 let source_list = SourceList(&self.value);
1370 if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1371 return Blocked;
1372 }
1373 Allowed
1374 }
1375 "style-src-attr" => {
1376 let name = get_the_effective_directive_for_inline_checks(type_);
1377 if !should_fetch_directive_execute(name, "style-src-attr", policy) {
1378 return Allowed;
1379 }
1380 let source_list = SourceList(&self.value);
1381 if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1382 return Blocked;
1383 }
1384 Allowed
1385 }
1386 _ => Allowed
1387 }
1388 }
1389 pub fn get_sandboxing_flag_set_for_document(&self, policy: &Policy) -> Option<SandboxingFlagSet> {
1391 debug_assert!(&self.name[..] == "sandbox");
1392 if policy.disposition != PolicyDisposition::Enforce {
1394 None
1395 } else {
1396 Some(parse_a_sandboxing_directive(&self.value[..]))
1398 }
1399 }
1400 pub fn pre_navigation_check<TrustedTypesUrlProcessor>(
1402 &self, request: &mut Request, type_: NavigationCheckType, url_processor: &TrustedTypesUrlProcessor, _policy: &Policy) -> CheckResult
1403 where
1404 TrustedTypesUrlProcessor: Fn(&str) -> Option<String>
1405 {
1406 use CheckResult::*;
1407 match &self.name[..] {
1408 "form-action" => {
1410 if type_ == NavigationCheckType::FormSubmission {
1412 let source_list = SourceList(&self.value);
1413 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1416 return Blocked;
1417 }
1418 }
1419 Allowed
1421 },
1422 "require-trusted-types-for" => {
1424 let url = &request.url;
1425 if url.scheme() != "javascript" {
1427 return Allowed;
1428 }
1429 let encoded_script_source = &url[Position::AfterScheme..][1..];
1434 let Some(converted_script_source) = url_processor(encoded_script_source) else {
1438 return Blocked;
1439 };
1440 let url_string = "javascript:".to_owned() + &converted_script_source;
1442 let Ok(new_url) = Url::parse(&url_string) else {
1445 return Blocked;
1446 };
1447 request.url = new_url;
1449 Allowed
1451 }
1452 _ => Allowed,
1453 }
1454 }
1455}
1456
1457fn get_the_effective_directive_for_inline_checks(type_: InlineCheckType) -> &'static str {
1459 use InlineCheckType::*;
1460 match type_ {
1461 Script | Navigation => "script-src-elem",
1462 ScriptAttribute => "script-src-attr",
1463 Style => "style-src-elem",
1464 StyleAttribute => "style-src-attr",
1465 }
1466}
1467
1468fn script_directives_prerequest_check(request: &Request, directive: &Directive) -> CheckResult {
1470 use CheckResult::*;
1471 if request_is_script_like(request) {
1473 let source_list = SourceList(&directive.value[..]);
1474 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1477 return Allowed;
1478 }
1479 if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata) == Matches {
1482 return Allowed;
1483 }
1484 if directive.value.iter().any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'")) {
1487 if request.parser_metadata == ParserMetadata::ParserInserted {
1489 return Blocked;
1490 }
1491 return Allowed;
1493 }
1494
1495 if source_list.does_request_match_source_list(request) == DoesNotMatch {
1498 return Blocked;
1499 }
1500 }
1501 Allowed
1503}
1504
1505fn script_directives_postrequest_check(request: &Request, response: &Response, directive: &Directive) -> CheckResult {
1507 use CheckResult::*;
1508 if request_is_script_like(request) {
1510 let source_list = SourceList(&directive.value[..]);
1513 if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1516 return Allowed;
1517 }
1518 if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata) == Matches {
1521 return Allowed;
1522 }
1523 if directive.value.iter().any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'")) {
1525 if request.parser_metadata == ParserMetadata::ParserInserted {
1527 return Blocked;
1528 }
1529 return Allowed;
1531 }
1532 if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1535 return Blocked;
1536 }
1537 }
1538 Allowed
1540}
1541
1542fn request_is_script_like(request: &Request) -> bool {
1544 request.destination.is_script_like()
1545}
1546
1547fn should_fetch_directive_execute(effective_directive_name: &str, directive_name: &str, policy: &Policy) -> bool {
1549 let directive_fallback_list = get_fetch_directive_fallback_list(effective_directive_name);
1550 for fallback_directive in directive_fallback_list {
1551 if directive_name == *fallback_directive {
1552 return true;
1553 }
1554 if policy.contains_a_directive_whose_name_is(fallback_directive) {
1555 return false;
1556 }
1557 }
1558 false
1559}
1560
1561fn get_fetch_directive_fallback_list(directive_name: &str) -> &'static [&'static str] {
1563 match directive_name {
1564 "script-src-elem" => &["script-src-elem", "script-src", "default-src"],
1565 "script-src-attr" => &["script-src-attr", "script-src", "default-src"],
1566 "style-src-elem" => &["style-src-elem", "style-src", "default-src"],
1567 "style-src-attr" => &["style-src-attr", "style-src", "default-src"],
1568 "worker-src" => &["worker-src", "child-src", "script-src", "default-src"],
1569 "connect-src" => &["connect-src", "default-src"],
1570 "manifest-src" => &["manifest-src", "default-src"],
1571 "object-src" => &["object-src", "default-src"],
1572 "frame-src" => &["frame-src", "child-src", "default-src"],
1573 "media-src" => &["media-src", "default-src"],
1574 "font-src" => &["font-src", "default-src"],
1575 "img-src" => &["img-src", "default-src"],
1576 _ => &[],
1577 }
1578}
1579
1580fn get_the_effective_directive_for_request(request: &Request) -> &'static str {
1582 use Initiator::*;
1583 use Destination::*;
1584 if request.initiator == Prefetch || request.initiator == Prerender {
1586 return "default-src";
1587 }
1588 match request.destination {
1590 Destination::Manifest => "manifest-src",
1591 Object | Embed => "object-src",
1592 Frame | IFrame => "frame-src",
1593 Audio | Track | Video => "media-src",
1594 Font => "font-src",
1595 Image => "img-src",
1596 Style => "style-src-elem",
1597 Script | Destination::Xslt | AudioWorklet | PaintWorklet => "script-src-elem",
1598 ServiceWorker | SharedWorker | Worker => "worker-src",
1599 Json | WebIdentity => "connect-src",
1600 Report => "",
1601 _ => "connect-src",
1603 }
1604}
1605
1606#[derive(Clone, Debug, Eq, PartialEq)]
1608pub enum MatchResult {
1609 Matches,
1610 DoesNotMatch,
1611}
1612
1613static DIRECTIVE_NAME_GRAMMAR: Lazy<Regex> =
1615 Lazy::new(|| Regex::new(r#"^[0-9a-z\-]+$"#).unwrap());
1616static DIRECTIVE_VALUE_TOKEN_GRAMMAR: Lazy<Regex> =
1618 Lazy::new(|| Regex::new(r#"^[\u{21}-\u{2B}\u{2D}-\u{3A}\u{3C}-\u{7E}]+$"#).unwrap());
1619static NONCE_SOURCE_GRAMMAR: Lazy<Regex> =
1621 Lazy::new(|| Regex::new(r#"^'nonce-(?P<n>[a-zA-Z0-9\+/\-_]+=*)'$"#).unwrap());
1622static NONE_SOURCE_GRAMMAR: Lazy<Regex> =
1623 Lazy::new(|| Regex::new(r#"^'none'$"#).unwrap());
1624static SCHEME_SOURCE_GRAMMAR: Lazy<Regex> =
1626 Lazy::new(|| Regex::new(r#"^(?P<scheme>[a-zA-Z][a-zA-Z0-9\+\-\.]*):$"#).unwrap());
1627static HOST_SOURCE_GRAMMAR: Lazy<Regex> =
1629 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());
1630static HASH_SOURCE_GRAMMAR: Lazy<Regex> =
1632 Lazy::new(|| Regex::new(r#"^'(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)'$"#).unwrap());
1633
1634#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1636struct SourceList<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item=&'a U>>(I);
1637
1638impl<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item=&'a U>> SourceList<'a, U, I> {
1639 fn does_nonce_match_source_list(&self, nonce: &str) -> MatchResult {
1641 if nonce.is_empty() { return DoesNotMatch };
1642 for expression in self.0.clone().into_iter() {
1643 if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression.borrow()) {
1644 if let Some(captured_nonce) = captures.name("n") {
1645 if nonce == captured_nonce.as_str() {
1646 return Matches;
1647 }
1648 }
1649 }
1650 }
1651 DoesNotMatch
1652 }
1653 fn does_integrity_metadata_match_source_list(&self, integrity_metadata: &str) -> MatchResult {
1655 let integrity_expressions: Vec<HashFunction> = self.0.clone().into_iter()
1657 .filter_map(|expression| {
1658 if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression.borrow()) {
1659 if let (Some(algorithm), Some(value)) = (captures.name("algorithm").and_then(|a| HashAlgorithm::from_name(a.as_str())), captures.name("value")) {
1660 return Some(HashFunction{ algorithm, value: String::from(value.as_str()) });
1661 }
1662 }
1663 None
1664 })
1665 .collect();
1666 if integrity_expressions.is_empty() {
1668 return DoesNotMatch;
1669 }
1670 let integrity_sources = parse_subresource_integrity_metadata(integrity_metadata);
1672 match integrity_sources {
1673 SubresourceIntegrityMetadata::NoMetadata => DoesNotMatch,
1675 SubresourceIntegrityMetadata::IntegritySources(integrity_sources) => {
1676 if integrity_sources.is_empty() {
1677 return DoesNotMatch;
1678 }
1679 for source in &integrity_sources {
1681 if !integrity_expressions.iter().any(|ex| ex == source) {
1688 return DoesNotMatch;
1689 }
1690 }
1691 Matches
1693 }
1694 }
1695 }
1696 fn does_request_match_source_list(&self, request: &Request) -> MatchResult {
1698 self.does_url_match_source_list_in_origin_with_redirect_count(
1699 &request.url,
1700 &request.origin,
1701 request.redirect_count,
1702 )
1703 }
1704 fn does_url_match_source_list_in_origin_with_redirect_count(
1706 &self,
1707 url: &Url,
1708 origin: &Origin,
1709 redirect_count: u32,
1710 ) -> MatchResult {
1711 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1712 if NONE_SOURCE_GRAMMAR.is_match(expression) { continue };
1713 let result = does_url_match_expression_in_origin_with_redirect_count(
1714 url,
1715 expression,
1716 origin,
1717 redirect_count
1718 );
1719 if result == Matches {
1720 return Matches;
1721 }
1722 }
1723 DoesNotMatch
1724 }
1725 fn does_element_match_source_list_for_type_and_source(
1727 &self,
1728 element: &Element,
1729 type_: InlineCheckType,
1730 source: &str,
1731 ) -> MatchResult {
1732 if self.does_a_source_list_allow_all_inline_behavior_for_type(type_) == AllowResult::Allows {
1733 return Matches;
1734 }
1735 if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style {
1736 if let Some(nonce) = element.nonce.as_ref() {
1737 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1738 if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression) {
1739 if let Some(captured_nonce) = captures.name("n") {
1740 if nonce == captured_nonce.as_str() {
1741 return Matches;
1742 }
1743 }
1744 }
1745 }
1746 }
1747 }
1748 let mut unsafe_hashes = false;
1749 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1750 if ascii_case_insensitive_match(expression, "'unsafe-hashes'") {
1751 unsafe_hashes = true;
1752 break;
1753 }
1754 }
1755 if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style || unsafe_hashes {
1756 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1757 if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression) {
1758 if let (Some(algorithm), Some(value)) = (captures.name("algorithm").and_then(|a| HashAlgorithm::from_name(a.as_str())), captures.name("value")) {
1759 let actual = algorithm.apply(source);
1760 let expected = value.as_str().replace('-', "+").replace('_', "/");
1761 if actual == expected {
1762 return Matches;
1763 }
1764 }
1765 }
1766 }
1767 }
1768 DoesNotMatch
1769 }
1770 fn does_a_source_list_allow_all_inline_behavior_for_type(&self, type_: InlineCheckType) -> AllowResult {
1772 use InlineCheckType::*;
1773 let mut allow_all_inline = false;
1774 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1775 if HASH_SOURCE_GRAMMAR.is_match(expression) || NONCE_SOURCE_GRAMMAR.is_match(expression) {
1776 return AllowResult::DoesNotAllow;
1777 }
1778 if (type_ == Script || type_ == ScriptAttribute || type_ == Navigation) && expression == "'strict-dynamic'" {
1779 return AllowResult::DoesNotAllow;
1780 }
1781 if ascii_case_insensitive_match(expression, "'unsafe-inline'") {
1782 allow_all_inline = true;
1783 }
1784 }
1785 if allow_all_inline {
1786 AllowResult::Allows
1787 } else {
1788 AllowResult::DoesNotAllow
1789 }
1790 }
1791 fn does_response_to_request_match_source_list(
1793 &self,
1794 request: &Request,
1795 response: &Response) -> MatchResult {
1796 self.does_url_match_source_list_in_origin_with_redirect_count(
1797 &response.url,
1798 &request.origin,
1799 response.redirect_count,
1800 )
1801 }
1802 fn does_a_source_list_allow_js_evaluation(&self) -> AllowResult {
1804 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1805 if ascii_case_insensitive_match(expression, "'unsafe-eval'") {
1808 return AllowResult::Allows;
1809 }
1810 }
1811 AllowResult::DoesNotAllow
1812 }
1813 fn does_a_source_list_allow_wasm_evaluation(&self) -> AllowResult {
1815 for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1816 if ascii_case_insensitive_match(expression, "'unsafe-eval'") ||
1817 ascii_case_insensitive_match(expression, "'wasm-unsafe-eval'") {
1818 return AllowResult::Allows;
1819 }
1820 }
1821 AllowResult::DoesNotAllow
1822 }
1823}
1824
1825#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1827enum AllowResult {
1828 Allows,
1829 DoesNotAllow,
1830}
1831
1832fn does_url_match_expression_in_origin_with_redirect_count(
1834 url: &Url,
1835 expression: &str,
1836 origin: &Origin,
1837 redirect_count: u32,
1838) -> MatchResult {
1839 let url_scheme = url.scheme();
1840 if expression == "*" {
1841 if scheme_is_network(url_scheme) {
1842 return Matches;
1843 }
1844 return origin_scheme_part_match(origin, url_scheme);
1845 }
1846 if let Some(captures) = SCHEME_SOURCE_GRAMMAR.captures(expression) {
1847 if let Some(expression_scheme) = captures.name("scheme") {
1848 return scheme_part_match(expression_scheme.as_str(), url_scheme);
1849 }
1850 return DoesNotMatch;
1852 }
1853 if let Some(captures) = HOST_SOURCE_GRAMMAR.captures(expression) {
1854 let expr_has_scheme_part = if let Some(expression_scheme) = captures.name("scheme") {
1855 if scheme_part_match(expression_scheme.as_str(), url_scheme) != Matches {
1856 return DoesNotMatch;
1857 }
1858 true
1859 } else {
1860 false
1861 };
1862 let url_host = if let Some(url_host) = url.host() {
1863 url_host
1864 } else {
1865 return DoesNotMatch;
1866 };
1867 if !expr_has_scheme_part &&
1868 origin_scheme_part_match(origin, url.scheme()) != Matches {
1869 return DoesNotMatch;
1870 }
1871 if let Some(expression_host) = captures.name("host") {
1872 if host_part_match(expression_host.as_str(), &url_host.to_string()) != Matches {
1873 return DoesNotMatch;
1874 }
1875 } else {
1876 return DoesNotMatch;
1878 }
1879 let port_part = captures.name("port").map(|port| &port.as_str()[1..]).unwrap_or("");
1881 let url_port = url_port(url);
1882 if port_part_match(port_part, &url_port[..], url.scheme()) != 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(port_a: &str, port_b: &str, scheme_b: &str) -> MatchResult {
1966 if port_a.is_empty() {
1967 if port_b == default_port_str(scheme_b) {
1968 return Matches;
1969 } else {
1970 return DoesNotMatch;
1971 }
1972 }
1973 if port_a == "*" {
1974 return Matches;
1975 }
1976 if port_a == port_b {
1977 return Matches;
1978 }
1979 if port_b.is_empty() {
1980 if port_a == default_port_str(scheme_b) {
1981 return Matches;
1982 } else {
1983 return DoesNotMatch;
1984 }
1985 }
1986 DoesNotMatch
1987}
1988
1989fn path_part_match(path_a: &str, path_b: &str) -> MatchResult {
1991 if path_a.is_empty() {
1992 return Matches;
1993 }
1994 if path_a == "/" && path_b.is_empty() {
1995 return Matches;
1996 }
1997 let exact_match = path_a.as_bytes()[path_a.len()-1] != b'/';
1998 let (mut path_list_a, path_list_b): (Vec<&str>, Vec<&str>) =
1999 (path_a.split('/').collect(), path_b.split('/').collect());
2000 if path_list_a.len() > path_list_b.len() {
2001 return DoesNotMatch;
2002 }
2003 if exact_match && path_list_a.len() != path_list_b.len() {
2004 return DoesNotMatch;
2005 }
2006 if !exact_match {
2007 debug_assert_eq!(path_list_a[path_list_a.len()-1], "");
2008 path_list_a.pop();
2009 }
2010 let mut piece_b_iter = path_list_b.iter();
2011 for piece_a in &path_list_a {
2012 let piece_b = piece_b_iter.next().unwrap();
2013 let piece_a: Vec<u8> = percent_encoding::percent_decode(piece_a.as_bytes()).collect();
2014 let piece_b: Vec<u8> = percent_encoding::percent_decode(piece_b.as_bytes()).collect();
2015 if piece_a != piece_b {
2016 return DoesNotMatch;
2017 }
2018 }
2019 Matches
2020}
2021
2022fn url_port(url: &Url) -> String {
2023 match url.port() {
2024 Some(num) => num.to_string(),
2025 None => default_port_str(url.scheme()).to_string(),
2026 }
2027}
2028
2029fn default_port_str(scheme: &str) -> &'static str {
2030 match scheme {
2031 "ftp" => "21",
2032 "gopher" => "70",
2033 "http" => "80",
2034 "https" => "443",
2035 "ws" => "80",
2036 "wss" => "443",
2037 _ => "",
2038 }
2039}
2040
2041fn default_port(scheme: &str) -> Option<u16> {
2042 Some(match scheme {
2043 "ftp" => 21,
2044 "gopher" => 70,
2045 "http" => 80,
2046 "https" => 443,
2047 "ws" => 80,
2048 "wss" => 443,
2049 _ => return None,
2050 })
2051}
2052
2053fn origin_scheme_part_match(a: &Origin, b: &str) -> MatchResult {
2054 if let Origin::Tuple(scheme, _host, _port) = a {
2055 scheme_part_match(&scheme[..], b)
2056 } else {
2057 DoesNotMatch
2058 }
2059}
2060
2061fn scheme_part_match(a: &str, b: &str) -> MatchResult {
2063 let a = a.to_ascii_lowercase();
2064 let b = b.to_ascii_lowercase();
2065 match (&a[..], &b[..]) {
2066 _ if a == b => Matches,
2067 ("http", "https") |
2068 ("ws", "wss") |
2069 ("wss", "https") => Matches,
2070 _ => DoesNotMatch,
2071 }
2072}
2073
2074#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2075pub enum HashAlgorithm {
2076 Sha256,
2077 Sha384,
2078 Sha512,
2079}
2080
2081impl HashAlgorithm {
2082 pub fn from_name(name: &str) -> Option<HashAlgorithm> {
2083 use HashAlgorithm::*;
2084 match name {
2085 "sha256" | "Sha256" | "sHa256" | "shA256" | "SHa256" | "ShA256" | "sHA256" | "SHA256" => Some(Sha256),
2086 "sha384" | "Sha384" | "sHa384" | "shA384" | "SHa384" | "ShA384" | "sHA384" | "SHA384" => Some(Sha384),
2087 "sha512" | "Sha512" | "sHa512" | "shA512" | "SHa512" | "ShA512" | "sHA512" | "SHA512" => Some(Sha512),
2088 _ => None,
2089 }
2090 }
2091 pub fn apply(self, value: &str) -> String {
2092 use base64::Engine as _;
2093 let bytes = value.as_bytes();
2094 let standard = base64::engine::general_purpose::STANDARD;
2095 match self {
2096 HashAlgorithm::Sha256 => standard.encode(sha2::Sha256::digest(bytes)),
2097 HashAlgorithm::Sha384 => standard.encode(sha2::Sha384::digest(bytes)),
2098 HashAlgorithm::Sha512 => standard.encode(sha2::Sha512::digest(bytes)),
2099 }
2100 }
2101}
2102
2103#[derive(Clone, Debug, Eq, PartialEq)]
2105pub struct HashFunction {
2106 algorithm: HashAlgorithm,
2107 value: String,
2108 }
2110
2111#[derive(Clone, Debug, Eq, PartialEq)]
2113pub enum SubresourceIntegrityMetadata {
2114 NoMetadata,
2115 IntegritySources(Vec<HashFunction>)
2116}
2117
2118static SUBRESOURCE_METADATA_GRAMMAR: Lazy<Regex> =
2121 Lazy::new(|| Regex::new(r#"(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)"#).unwrap());
2122
2123pub fn parse_subresource_integrity_metadata(string: &str) -> SubresourceIntegrityMetadata {
2125 let mut result = Vec::new();
2126 let mut empty = true;
2127 for token in split_ascii_whitespace(string) {
2128 empty = false;
2129 if let Some(captures) = SUBRESOURCE_METADATA_GRAMMAR.captures(token) {
2130 if let (Some(algorithm), Some(value)) = (captures.name("algorithm").and_then(|a| HashAlgorithm::from_name(a.as_str())), captures.name("value")) {
2131 result.push(HashFunction{ algorithm, value: String::from(value.as_str()) });
2132 }
2133 }
2134 }
2135 if empty {
2136 SubresourceIntegrityMetadata::NoMetadata
2137 } else {
2138 SubresourceIntegrityMetadata::IntegritySources(result)
2139 }
2140}
2141
2142#[cfg(test)]
2143mod test {
2144 use super::*;
2145 #[test]
2146 fn empty_directive_is_not_valid() {
2147 let d = Directive {
2148 name: String::new(),
2149 value: Vec::new(),
2150 };
2151 assert!(!d.is_valid());
2152 }
2153 #[test]
2154 pub fn duplicate_policy_is_not_valid() {
2155 let d = Directive {
2156 name: "test".to_owned(),
2157 value: vec!["test".to_owned()],
2158 };
2159 let p = Policy {
2160 directive_set: vec![d.clone(), d.clone()],
2161 disposition: PolicyDisposition::Enforce,
2162 source: PolicySource::Header,
2163 };
2164 assert!(!p.is_valid());
2165 }
2166 #[test]
2167 pub fn basic_policy_is_valid() {
2168 let p = Policy::parse("script-src notriddle.com", PolicySource::Header, PolicyDisposition::Enforce);
2169 assert!(p.is_valid());
2170 }
2171 #[test]
2172 pub fn policy_with_empty_directive_set_is_not_valid() {
2173 let p = Policy {
2174 directive_set: vec![],
2175 disposition: PolicyDisposition::Enforce,
2176 source: PolicySource::Header,
2177 };
2178 assert!(!p.is_valid());
2179 }
2180
2181 #[test]
2182 pub fn prefetch_request_does_not_violate_policy() {
2183 let request = Request {
2184 url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
2185 origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
2186 redirect_count: 0,
2187 destination: Destination::Script,
2188 initiator: Initiator::Prefetch,
2189 nonce: String::new(),
2190 integrity_metadata: String::new(),
2191 parser_metadata: ParserMetadata::None,
2192 };
2193
2194 let p = Policy::parse("child-src 'self'", PolicySource::Header, PolicyDisposition::Enforce);
2195
2196 let violation_result = p.does_request_violate_policy(&request);
2197
2198 assert!(violation_result == Violates::DoesNotViolate);
2199 }
2200
2201 #[test]
2202 pub fn prefetch_request_violates_policy() {
2203 let request = Request {
2204 url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
2205 origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
2206 redirect_count: 0,
2207 destination: Destination::ServiceWorker,
2208 initiator: Initiator::None,
2209 nonce: String::new(),
2210 integrity_metadata: String::new(),
2211 parser_metadata: ParserMetadata::None,
2212 };
2213
2214 let p = Policy::parse("default-src 'none'; script-src 'self' ", PolicySource::Header, PolicyDisposition::Enforce);
2215
2216 let violation_result = p.does_request_violate_policy(&request);
2217
2218 let expected_result = Violates::Directive(Directive { name: String::from("script-src"), value: vec![String::from("'self'")] });
2219
2220 assert!(violation_result == expected_result);
2221 }
2222
2223 #[test]
2224 pub fn prefetch_request_is_allowed_by_directive() {
2225 let request = Request {
2226 url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
2227 origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
2228 redirect_count: 0,
2229 destination: Destination::Script,
2230 initiator: Initiator::Prefetch,
2231 nonce: String::new(),
2232 integrity_metadata: String::new(),
2233 parser_metadata: ParserMetadata::None,
2234 };
2235
2236 let p = Policy::parse("default-src 'none'; child-src 'self'", PolicySource::Header, PolicyDisposition::Enforce);
2237
2238 let violation_result = p.does_request_violate_policy(&request);
2239
2240 assert!(violation_result == Violates::DoesNotViolate);
2241 }
2242
2243 #[test]
2244 pub fn trusted_type_policy_is_valid() {
2245 let p = Policy::parse("trusted-types 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2246 assert!(p.is_valid());
2247 assert_eq!(p.directive_set[0].value, vec!["'none'".to_owned()]);
2248 }
2249
2250 #[test]
2251 pub fn csp_list_is_valid() {
2252 let csp_list = CspList::parse("default-src 'none'; child-src 'self', trusted-types 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2253 assert!(csp_list.is_valid());
2254 assert_eq!(csp_list.0[1].directive_set[0].value, vec!["'none'".to_owned()]);
2255 }
2256
2257 #[test]
2258 pub fn no_trusted_types_specified_allows_all_policies() {
2259 let csp_list = CspList::parse("default-src 'none'; child-src 'self'", PolicySource::Meta, PolicyDisposition::Enforce);
2260 assert!(csp_list.is_valid());
2261 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec![]);
2262 assert_eq!(check_result, CheckResult::Allowed);
2263 assert!(violations.is_empty());
2264 }
2265
2266 #[test]
2267 pub fn none_does_not_allow_for_any_policy() {
2268 let csp_list = CspList::parse("trusted-types 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2269 assert!(csp_list.is_valid());
2270 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("some-policy", &vec![]);
2271 assert!(check_result == CheckResult::Blocked);
2272 assert_eq!(violations.len(), 1);
2273 }
2274
2275 #[test]
2276 pub fn extra_none_allows_all_policies() {
2277 let csp_list = CspList::parse("trusted-types some-policy 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2278 assert!(csp_list.is_valid());
2279 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("some-policy", &vec![]);
2280 assert!(check_result == CheckResult::Allowed);
2281 assert!(violations.is_empty());
2282 }
2283
2284 #[test]
2285 pub fn explicit_policy_named_is_allowed() {
2286 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2287 assert!(csp_list.is_valid());
2288 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec![]);
2289 assert_eq!(check_result, CheckResult::Allowed);
2290 assert!(violations.is_empty());
2291 }
2292
2293 #[test]
2294 pub fn other_policy_name_is_blocked() {
2295 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2296 assert!(csp_list.is_valid());
2297 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &vec![]);
2298 assert!(check_result == CheckResult::Blocked);
2299 assert_eq!(violations.len(), 1);
2300 }
2301
2302 #[test]
2303 pub fn invalid_characters_in_policy_name_is_blocked() {
2304 let csp_list = CspList::parse("trusted-types My?Policy", PolicySource::Meta, PolicyDisposition::Enforce);
2305 assert!(csp_list.is_valid());
2306 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("My?Policy", &vec!["My?Policy"]);
2307 assert!(check_result == CheckResult::Blocked);
2308 assert_eq!(violations.len(), 1);
2309 }
2310
2311 #[test]
2312 pub fn already_created_policy_is_blocked() {
2313 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2314 assert!(csp_list.is_valid());
2315 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec!["MyPolicy"]);
2316 assert!(check_result == CheckResult::Blocked);
2317 assert_eq!(violations.len(), 1);
2318 }
2319
2320 #[test]
2321 pub fn already_created_policy_is_allowed_with_allow_duplicates() {
2322 let csp_list = CspList::parse("trusted-types MyPolicy 'allow-duplicates'", PolicySource::Meta, PolicyDisposition::Enforce);
2323 assert!(csp_list.is_valid());
2324 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec!["MyPolicy"]);
2325 assert!(check_result == CheckResult::Allowed);
2326 assert!(violations.is_empty());
2327 }
2328
2329 #[test]
2330 pub fn only_report_policy_issues_for_disposition_report() {
2331 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Report);
2332 assert!(csp_list.is_valid());
2333 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec!["MyPolicy"]);
2334 assert!(check_result == CheckResult::Allowed);
2335 assert_eq!(violations.len(), 1);
2336 }
2337
2338 #[test]
2339 pub fn wildcard_allows_all_policies() {
2340 let csp_list = CspList::parse("trusted-types *", PolicySource::Meta, PolicyDisposition::Report);
2341 assert!(csp_list.is_valid());
2342 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec![]);
2343 assert!(check_result == CheckResult::Allowed);
2344 assert!(violations.is_empty());
2345 }
2346
2347 #[test]
2348 pub fn violation_has_correct_directive() {
2349 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2350 assert!(csp_list.is_valid());
2351 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &vec![]);
2352 assert!(check_result == CheckResult::Blocked);
2353 assert_eq!(violations.len(), 1);
2354 assert_eq!(violations[0].directive, csp_list.0[0].directive_set[0]);
2355 }
2356
2357 #[test]
2358 pub fn long_policy_name_is_truncated() {
2359 let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2360 assert!(csp_list.is_valid());
2361 let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("SuperLongPolicyNameThatExceeds40Characters", &vec![]);
2362 assert!(check_result == CheckResult::Blocked);
2363 assert_eq!(violations.len(), 1);
2364 assert!(matches!(&violations[0].resource, ViolationResource::TrustedTypePolicy { sample } if sample == "SuperLongPolicyNameThatExceeds40Characte"));
2365 }
2366}