Skip to main content

content_security_policy/
lib.rs

1/*!
2Parse and validate Web [Content-Security-Policy level 3](https://www.w3.org/TR/CSP/)
3
4# Example
5
6```rust
7extern crate content_security_policy;
8use content_security_policy::*;
9fn main() {
10    let csp_list = CspList::parse("script-src *.notriddle.com", PolicySource::Header, PolicyDisposition::Enforce);
11    let (check_result, _) = csp_list.should_request_be_blocked(&Request {
12        url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
13        current_url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
14        origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
15        redirect_count: 0,
16        destination: Destination::Script,
17        initiator: Initiator::None,
18        nonce: String::new(),
19        integrity_metadata: String::new(),
20        parser_metadata: ParserMetadata::None,
21    });
22    assert_eq!(check_result, CheckResult::Allowed);
23    let (check_result, _) = csp_list.should_request_be_blocked(&Request {
24        url: Url::parse("https://www.evil.example/script.js").unwrap(),
25        current_url: Url::parse("https://www.evil.example/script.js").unwrap(),
26        origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
27        redirect_count: 0,
28        destination: Destination::Script,
29        initiator: Initiator::None,
30        nonce: String::new(),
31        integrity_metadata: String::new(),
32        parser_metadata: ParserMetadata::None,
33    });
34    assert_eq!(check_result, CheckResult::Blocked);
35}
36```
37*/
38
39#![forbid(unsafe_code)]
40
41pub extern crate percent_encoding;
42pub extern crate url;
43
44pub mod sandboxing_directive;
45pub(crate) mod text_util;
46
47use once_cell::sync::Lazy;
48use regex::Regex;
49use sandboxing_directive::{parse_a_sandboxing_directive, SandboxingFlagSet};
50#[cfg(feature = "serde")]
51use serde::{Deserialize, Serialize};
52use sha2::Digest;
53use std::borrow::{Borrow, Cow};
54use std::cmp;
55use std::collections::HashSet;
56use std::fmt::{self, Display, Formatter};
57use std::str::FromStr;
58use text_util::{
59    ascii_case_insensitive_match, collect_a_sequence_of_non_ascii_white_space_code_points,
60    split_ascii_whitespace, split_commas, strip_leading_and_trailing_ascii_whitespace,
61};
62pub use url::{Origin, Position, Url};
63use MatchResult::DoesNotMatch;
64use MatchResult::Matches;
65
66fn scheme_is_network(scheme: &str) -> bool {
67    scheme == "ftp" || scheme_is_httpx(scheme)
68}
69
70fn scheme_is_httpx(scheme: &str) -> bool {
71    scheme == "http" || scheme == "https"
72}
73
74/**
75A single parsed content security policy.
76
77https://www.w3.org/TR/CSP/#content-security-policy-object
78*/
79#[derive(Clone, Debug)]
80#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
81pub struct Policy {
82    pub directive_set: Vec<Directive>,
83    pub disposition: PolicyDisposition,
84    pub source: PolicySource,
85}
86
87impl Display for Policy {
88    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
89        for (i, directive) in self.directive_set.iter().enumerate() {
90            if i != 0 {
91                write!(f, "; ")?;
92            }
93            <Directive as Display>::fmt(directive, f)?;
94        }
95        Ok(())
96    }
97}
98
99impl Policy {
100    pub fn is_valid(&self) -> bool {
101        self.directive_set.iter().all(Directive::is_valid)
102            && self
103                .directive_set
104                .iter()
105                .map(|d| d.name.clone())
106                .collect::<HashSet<_>>()
107                .len()
108                == self.directive_set.len()
109            && !self.directive_set.is_empty()
110    }
111    /// https://www.w3.org/TR/CSP/#parse-serialized-policy
112    pub fn parse(serialized: &str, source: PolicySource, disposition: PolicyDisposition) -> Policy {
113        let mut policy = Policy {
114            directive_set: Vec::new(),
115            source,
116            disposition,
117        };
118        // Rust's str::split corresponds to a WHATWG "strict split"
119        for token in serialized.split(';') {
120            let token = strip_leading_and_trailing_ascii_whitespace(token);
121            if token.is_empty() {
122                continue;
123            };
124            let (directive_name, token) =
125                collect_a_sequence_of_non_ascii_white_space_code_points(token);
126            let mut directive_name = directive_name.to_owned();
127            directive_name.make_ascii_lowercase();
128            if policy.contains_a_directive_whose_name_is(&directive_name) {
129                continue;
130            }
131            let directive_value = split_ascii_whitespace(token).map(String::from).collect();
132            policy.directive_set.push(Directive {
133                name: directive_name,
134                value: directive_value,
135            });
136        }
137        policy
138    }
139    pub fn contains_a_directive_whose_name_is(&self, directive_name: &str) -> bool {
140        self.directive_set.iter().any(|d| d.name == directive_name)
141    }
142    /// https://www.w3.org/TR/CSP/#does-request-violate-policy
143    pub fn does_request_violate_policy(&self, request: &Request) -> Violates {
144        if request.initiator == Initiator::Prefetch {
145            return self.does_resource_hint_violate_policy(request);
146        }
147
148        let mut violates = Violates::DoesNotViolate;
149        for directive in &self.directive_set {
150            let result = directive.pre_request_check(request, self);
151            if result == CheckResult::Blocked {
152                violates = Violates::Directive(directive.clone());
153            }
154        }
155        violates
156    }
157
158    /// https://www.w3.org/TR/CSP/#does-resource-hint-violate-policy
159    pub fn does_resource_hint_violate_policy(&self, request: &Request) -> Violates {
160        let default_directive = &self.directive_set.iter().find(|x| x.name == "default-src");
161
162        if default_directive.is_none() {
163            return Violates::DoesNotViolate;
164        }
165
166        for directive in &self.directive_set {
167            let result = directive.pre_request_check(request, self);
168            if result == CheckResult::Allowed {
169                return Violates::DoesNotViolate;
170            }
171        }
172
173        return Violates::Directive(default_directive.unwrap().clone());
174    }
175}
176
177#[derive(Clone, Debug)]
178#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
179/// https://www.w3.org/TR/CSP/#csp-list
180pub struct CspList(pub Vec<Policy>);
181
182impl Display for CspList {
183    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
184        for (i, directive) in self.0.iter().enumerate() {
185            if i != 0 {
186                write!(f, ",")?;
187            }
188            <Policy as Display>::fmt(directive, f)?;
189        }
190        Ok(())
191    }
192}
193
194/// https://www.w3.org/TR/trusted-types/#trusted-types-csp-directive
195static TRUSTED_POLICY_SOURCE_GRAMMAR: Lazy<Regex> =
196    Lazy::new(|| Regex::new(r#"^[0-9a-zA-Z\-\#=_\/@\.%]+$"#).unwrap());
197
198impl CspList {
199    pub fn is_valid(&self) -> bool {
200        self.0.iter().all(Policy::is_valid)
201    }
202    /// https://www.w3.org/TR/CSP/#contains-a-header-delivered-content-security-policy
203    pub fn contains_a_header_delivered_content_security_policy(&self) -> bool {
204        self.0
205            .iter()
206            .any(|policy| policy.source == PolicySource::Header)
207    }
208    /// https://www.w3.org/TR/CSP/#parse-serialized-policy-list
209    pub fn parse(list: &str, source: PolicySource, disposition: PolicyDisposition) -> CspList {
210        let mut policies = Vec::new();
211        for token in split_commas(list) {
212            let policy = Policy::parse(token, source, disposition);
213            if policy.directive_set.is_empty() {
214                continue;
215            };
216            policies.push(policy)
217        }
218        CspList(policies)
219    }
220    pub fn append(&mut self, mut other: CspList) {
221        self.0.append(&mut other.0)
222    }
223    pub fn push(&mut self, policy: Policy) {
224        self.0.push(policy)
225    }
226    /**
227    Given a request, this algorithm reports violations based on client’s "report only" policies.
228
229    https://www.w3.org/TR/CSP/#report-for-request
230    */
231    pub fn report_violations_for_request(&self, request: &Request) -> Vec<Violation> {
232        let mut violations = Vec::new();
233        for policy in &self.0 {
234            if policy.disposition == PolicyDisposition::Enforce {
235                continue;
236            };
237            let violates = policy.does_request_violate_policy(request);
238            if let Violates::Directive(directive) = violates {
239                let resource = ViolationResource::Url(request.url.clone());
240                violations.push(Violation {
241                    resource,
242                    directive: Directive {
243                        name: get_the_effective_directive_for_request(request).to_owned(),
244                        value: directive.value.clone(),
245                    },
246                    policy: policy.clone(),
247                });
248            }
249        }
250        violations
251    }
252    /**
253    Given a request, this algorithm returns Blocked or Allowed and reports violations based on
254    request’s client’s Content Security Policy.
255
256    https://www.w3.org/TR/CSP/#should-block-request
257    */
258    pub fn should_request_be_blocked(&self, request: &Request) -> (CheckResult, Vec<Violation>) {
259        let mut result = CheckResult::Allowed;
260        let mut violations = Vec::new();
261        for policy in &self.0 {
262            if policy.disposition == PolicyDisposition::Report {
263                continue;
264            };
265            let violates = policy.does_request_violate_policy(request);
266            if let Violates::Directive(directive) = violates {
267                result = CheckResult::Blocked;
268                let resource = ViolationResource::Url(request.url.clone());
269                violations.push(Violation {
270                    resource,
271                    directive: Directive {
272                        name: get_the_effective_directive_for_request(request).to_owned(),
273                        value: directive.value.clone(),
274                    },
275                    policy: policy.clone(),
276                });
277            }
278        }
279        (result, violations)
280    }
281    /**
282    Given a response and a request, this algorithm returns Blocked or Allowed, and reports
283    violations based on request’s client’s Content Security Policy.
284
285    https://www.w3.org/TR/CSP/#should-block-response
286    */
287    pub fn should_response_to_request_be_blocked(
288        &self,
289        request: &Request,
290        response: &Response,
291    ) -> (CheckResult, Vec<Violation>) {
292        // Step 1. Let CSP list be request’s policy container’s CSP list.
293        // step 2. Let result be "Allowed".
294        let mut result = CheckResult::Allowed;
295        let mut violations = Vec::new();
296        // Step 3. For each policy of CSP list:
297        for policy in &self.0 {
298            // Step 3.1. For each directive of policy:
299            for directive in &policy.directive_set {
300                // Step 3.1.1. If the result of executing directive’s post-request check is "Blocked", then:
301                if directive.post_request_check(request, response, policy) == CheckResult::Blocked {
302                    // Step 3.1.1.1. Execute §5.5 Report a violation on the result of executing
303                    // §2.4.2 Create a violation object for request, and policy. on request, and policy.
304                    violations.push(Violation {
305                        resource: ViolationResource::Url(request.url.clone()),
306                        directive: Directive {
307                            name: get_the_effective_directive_for_request(request).to_owned(),
308                            value: directive.value.clone(),
309                        },
310                        policy: policy.clone(),
311                    });
312                    // Step 3.1.1.2. If policy’s disposition is "enforce", then set result to "Blocked".
313                    if policy.disposition == PolicyDisposition::Enforce {
314                        result = CheckResult::Blocked;
315                    }
316                }
317            }
318        }
319        (result, violations)
320    }
321    /// https://www.w3.org/TR/CSP/#should-block-inline
322    pub fn should_elements_inline_type_behavior_be_blocked(
323        &self,
324        element: &Element,
325        type_: InlineCheckType,
326        source: &str,
327    ) -> (CheckResult, Vec<Violation>) {
328        use CheckResult::*;
329        let mut result = Allowed;
330        let mut violations = Vec::new();
331        for policy in &self.0 {
332            for directive in &policy.directive_set {
333                if directive.inline_check(element, type_, policy, source) == Allowed {
334                    continue;
335                }
336                let sample = if directive.value.iter().any(|t| &t[..] == "'report-sample'") {
337                    let max_length = cmp::min(40, source.len());
338                    Some(source[0..max_length].to_owned())
339                } else {
340                    None
341                };
342                let violation = Violation {
343                    resource: ViolationResource::Inline { sample },
344                    directive: Directive {
345                        name: get_the_effective_directive_for_inline_checks(type_).to_owned(),
346                        value: directive.value.clone(),
347                    },
348                    policy: policy.clone(),
349                };
350                violations.push(violation);
351                if policy.disposition == PolicyDisposition::Enforce {
352                    result = Blocked;
353                }
354            }
355        }
356        (result, violations)
357    }
358    /**
359    https://www.w3.org/TR/CSP/#allow-base-for-document
360
361    Note that, while this algoritm is defined as operating on a document, the only property it
362    actually uses is the document's CSP List. So this function operates on that.
363    */
364    pub fn is_base_allowed_for_document(
365        &self,
366        base: &Url,
367        self_origin: &Origin,
368    ) -> (CheckResult, Vec<Violation>) {
369        use CheckResult::*;
370        let mut violations = Vec::new();
371        for policy in &self.0 {
372            let directive = policy
373                .directive_set
374                .iter()
375                .find(|directive| directive.name == "base-uri");
376            if let Some(directive) = directive {
377                if SourceList(&directive.value)
378                    .does_url_match_source_list_in_origin_with_redirect_count(base, &self_origin, 0)
379                    == DoesNotMatch
380                {
381                    let violation = Violation {
382                        directive: directive.clone(),
383                        resource: ViolationResource::Inline { sample: None },
384                        policy: policy.clone(),
385                    };
386                    violations.push(violation);
387                    if policy.disposition == PolicyDisposition::Enforce {
388                        return (Blocked, violations);
389                    }
390                }
391            }
392        }
393        return (Allowed, violations);
394    }
395
396    /**
397    https://w3c.github.io/trusted-types/dist/spec/#should-block-create-policy
398
399    Note that, while this algoritm is defined as operating on a global object, the only property it
400    actually uses is the global's CSP List. So this function operates on that.
401    */
402    pub fn is_trusted_type_policy_creation_allowed(
403        &self,
404        policy_name: &str,
405        created_policy_names: &[&str],
406    ) -> (CheckResult, Vec<Violation>) {
407        use CheckResult::*;
408        // Step 1: Let result be "Allowed".
409        let mut result = Allowed;
410        let mut violations = Vec::new();
411        // Step 2: For each policy in global’s CSP list:
412        for policy in &self.0 {
413            // Step 2.1: Let createViolation be false.
414            let mut create_violation = false;
415            // Step 2.2: If policy’s directive set does not contain a directive which name is "trusted-types", skip to the next policy.
416            let directive = policy
417                .directive_set
418                .iter()
419                .find(|directive| directive.name == "trusted-types");
420            // Step 2.3: Let directive be the policy’s directive set’s directive which name is "trusted-types"
421            if let Some(directive) = directive {
422                // Step 2.4: If directive’s value only contains a tt-keyword which is a match for a value 'none', set createViolation to true.
423                if directive.value.len() == 1 && directive.value.contains(&"'none'".to_string()) {
424                    create_violation = true;
425                }
426                // Step 2.5: If createdPolicyNames contains policyName and directive’s value does not contain a tt-keyword
427                // which is a match for a value 'allow-duplicates', set createViolation to true.
428                if created_policy_names.contains(&policy_name)
429                    && !directive.value.iter().any(|v| v == "'allow-duplicates'")
430                {
431                    create_violation = true;
432                }
433                // Step 2.6: If directive’s value does not contain a tt-policy-name, which value is policyName,
434                // and directive’s value does not contain a tt-wildcard, set createViolation to true.
435                if !(TRUSTED_POLICY_SOURCE_GRAMMAR.is_match(&policy_name)
436                    && (directive.value.iter().any(|p| p == policy_name)
437                        || directive.value.iter().any(|v| v == "*")))
438                {
439                    create_violation = true;
440                }
441                // Step 2.7: If createViolation is false, skip to the next policy.
442                if !create_violation {
443                    continue;
444                }
445                let max_length = cmp::min(40, policy_name.len());
446                // Step 2.10: Set violation’s sample to the substring of policyName, containing its first 40 characters.
447                let sample = policy_name[0..max_length].to_owned();
448                // Step 2.8: Let violation be the result of executing Create a violation object for global, policy,
449                // and directive on global, policy and "trusted-types"
450                let violation = Violation {
451                    directive: directive.clone(),
452                    // Step 2.9: Set violation’s resource to "trusted-types-policy".
453                    resource: ViolationResource::TrustedTypePolicy {
454                        // Step 2.10: Set violation’s sample to the substring of policyName, containing its first 40 characters.
455                        sample,
456                    },
457                    policy: policy.clone(),
458                };
459                // Step 2.11: Execute Report a violation on violation.
460                violations.push(violation);
461                // Step 2.12: If policy’s disposition is "enforce", then set result to "Blocked".
462                if policy.disposition == PolicyDisposition::Enforce {
463                    result = Blocked
464                }
465            }
466        }
467        return (result, violations);
468    }
469    /**
470    https://w3c.github.io/trusted-types/dist/spec/#abstract-opdef-does-sink-type-require-trusted-types
471
472    Note that, while this algoritm is defined as operating on a global object, the only property it
473    actually uses is the global's CSP List. So this function operates on that.
474    */
475    pub fn does_sink_type_require_trusted_types(
476        &self,
477        sink_group: &str,
478        include_report_only_policies: bool,
479    ) -> bool {
480        let sink_group = &sink_group.to_owned();
481        // Step 1: For each policy in global’s CSP list:
482        for policy in &self.0 {
483            // Step 1.1: If policy’s directive set does not contain a directive whose name is "require-trusted-types-for", skip to the next policy.
484            let directive = policy
485                .directive_set
486                .iter()
487                .find(|directive| directive.name == "require-trusted-types-for");
488            // Step 1.2: Let directive be the policy’s directive set’s directive whose name is "require-trusted-types-for"
489            if let Some(directive) = directive {
490                // Step 1.3: If directive’s value does not contain a trusted-types-sink-group which is a match for sinkGroup, skip to the next policy.
491                if !directive.value.contains(sink_group) {
492                    continue;
493                }
494                // Step 1.4: Let enforced be true if policy’s disposition is "enforce", and false otherwise.
495                let enforced = policy.disposition == PolicyDisposition::Enforce;
496                // Step 1.5: If enforced is true, return true.
497                if enforced {
498                    return true;
499                }
500                // Step 1.6: If includeReportOnlyPolicies is true, return true.
501                if include_report_only_policies {
502                    return true;
503                }
504            }
505        }
506        // Step 2: Return false.
507        false
508    }
509    /**
510    https://w3c.github.io/trusted-types/dist/spec/#should-block-sink-type-mismatch
511
512    Note that, while this algoritm is defined as operating on a global object, the only property it
513    actually uses is the global's CSP List. So this function operates on that.
514    */
515    pub fn should_sink_type_mismatch_violation_be_blocked_by_csp(
516        &self,
517        sink: &str,
518        sink_group: &str,
519        source: &str,
520    ) -> (CheckResult, Vec<Violation>) {
521        use CheckResult::*;
522        let sink_group = &sink_group.to_owned();
523        // Step 1: Let result be "Allowed".
524        let mut result = Allowed;
525        let mut violations = Vec::new();
526        // Step 2: Let sample be source.
527        let mut sample = source;
528        // Step 3: If sink is "Function", then:
529        if sink == "Function" {
530            // Step 3.1: If sample starts with "function anonymous", strip that from sample.
531            if sample.starts_with("function anonymous") {
532                sample = &sample[18..];
533                // Step 3.2: Otherwise if sample starts with "async function anonymous", strip that from sample.
534            } else if sample.starts_with("async function anonymous") {
535                sample = &sample[24..];
536                // Step 3.3: Otherwise if sample starts with "function* anonymous", strip that from sample.
537            } else if sample.starts_with("function* anonymous") {
538                sample = &sample[19..];
539                // Step 3.4: Otherwise if sample starts with "async function* anonymous", strip that from sample.
540            } else if sample.starts_with("async function* anonymous") {
541                sample = &sample[25..];
542            }
543        }
544        // Step 4: For each policy in global’s CSP list:
545        for policy in &self.0 {
546            // Step 4.1: If policy’s directive set does not contain a directive whose name is "require-trusted-types-for", skip to the next policy.
547            let directive = policy
548                .directive_set
549                .iter()
550                .find(|directive| directive.name == "require-trusted-types-for");
551            // Step 4.2: Let directive be the policy’s directive set’s directive whose name is "require-trusted-types-for"
552            let Some(directive) = directive else { continue };
553            // Step 4.3: If directive’s value does not contain a trusted-types-sink-group which is a match for sinkGroup, skip to the next policy.
554            if !directive.value.contains(sink_group) {
555                continue;
556            }
557            // Step 4.6: Let trimmedSample be the substring of sample, containing its first 40 characters.
558            let mut trimmed_sample: String = sample.into();
559            trimmed_sample.truncate(40);
560            // Step 4.4: Let violation be the result of executing Create a violation object for global, policy,
561            // and directive on global, policy and "require-trusted-types-for"
562            violations.push(Violation {
563                // Step 4.5: Set violation’s resource to "trusted-types-sink".
564                resource: ViolationResource::TrustedTypeSink {
565                    // Step 4.7: Set violation’s sample to be the result of concatenating the list « sink, trimmedSample « using "|" as a separator.
566                    sample: sink.to_owned() + "|" + &trimmed_sample,
567                },
568                directive: directive.clone(),
569                policy: policy.clone(),
570            });
571            // Step 4.9: If policy’s disposition is "enforce", then set result to "Blocked".
572            if policy.disposition == PolicyDisposition::Enforce {
573                result = Blocked
574            }
575        }
576        // Step 2: Return false.
577        (result, violations)
578    }
579    /// <https://html.spec.whatwg.org/multipage/#csp-derived-sandboxing-flags>
580    pub fn get_sandboxing_flag_set_for_document(&self) -> Option<SandboxingFlagSet> {
581        // Step 1. Let directives be an empty ordered set.
582        // Step 2. For each policy in cspList:
583        self.0
584            .iter()
585            .flat_map(|policy| {
586                policy
587                    .directive_set
588                    .iter()
589                    // Step 4. Let directive be directives[directives's size − 1].
590                    .rev()
591                    // Step 2.2. If policy's directive set contains a directive whose name is "sandbox",
592                    // then append that directive to directives.
593                    .find(|directive| directive.name == "sandbox")
594                    .and_then(|directive| directive.get_sandboxing_flag_set_for_document(policy))
595            })
596            // Step 3. If directives is empty, then return an empty sandboxing flag set.
597            .next()
598    }
599    /// https://www.w3.org/TR/CSP/#can-compile-strings
600    pub fn is_js_evaluation_allowed(&self, source: &str) -> (CheckResult, Vec<Violation>) {
601        let mut result = CheckResult::Allowed;
602        let mut violations = Vec::new();
603        // Step 5: For each policy of global’s CSP list:
604        for policy in &self.0 {
605            // Step 5.1: Let source-list be null.
606            let directive = policy
607                .directive_set
608                .iter()
609                // Step 5.2: If policy contains a directive whose name is "script-src",
610                // then set source-list to that directive’s value.
611                .find(|directive| directive.name == "script-src")
612                // Step 5.2: Otherwise if policy contains a directive whose name is "default-src",
613                // then set source-list to that directive’s value.
614                .or_else(|| {
615                    policy
616                        .directive_set
617                        .iter()
618                        .find(|directive| directive.name == "default-src")
619                });
620            // Step 5.3: If source-list is not null:
621            let Some(directive) = directive else { continue };
622            let source_list = SourceList(&directive.value);
623            if source_list.does_a_source_list_allow_js_evaluation() == AllowResult::Allows {
624                continue;
625            }
626            // Step 5.3.1: Let trustedTypesRequired be the result of executing
627            // Does sink type require trusted types?, with realm, 'script', and false.
628            let trusted_types_required =
629                self.does_sink_type_require_trusted_types("'script'", false);
630            // Step 5.3.2: If trustedTypesRequired is true and source-list contains a source expression
631            // which is an ASCII case-insensitive match for the string "'trusted-types-eval'", then skip the following steps.
632            if trusted_types_required
633                && directive
634                    .value
635                    .iter()
636                    .any(|t| ascii_case_insensitive_match(&t[..], "'trusted-types-eval'"))
637            {
638                continue;
639            }
640            // Step 5.3.3: If source-list contains a source expression which is
641            // an ASCII case-insensitive match for the string "'unsafe-eval'", then skip the following steps.
642            if directive
643                .value
644                .iter()
645                .any(|t| ascii_case_insensitive_match(&t[..], "'unsafe-eval'"))
646            {
647                continue;
648            }
649            // Step 5.3.6: If source-list contains the expression "'report-sample'",
650            // then set violation’s sample to the substring of sourceString containing its first 40 characters.
651            let sample = if directive.value.iter().any(|t| &t[..] == "'report-sample'") {
652                let max_length = cmp::min(40, source.len());
653                Some(source[0..max_length].to_owned())
654            } else {
655                None
656            };
657            // Step 5.3.4: Let violation be the result of executing Create a violation object for global, policy,
658            // and directive on global, policy and "require-trusted-types-for"
659            violations.push(Violation {
660                // Step 5.3.5: Set violation’s resource to "eval".
661                resource: ViolationResource::Eval { sample },
662                directive: directive.clone(),
663                policy: policy.clone(),
664            });
665            // Step 5.3.8: If policy’s disposition is "enforce", then set result to "Blocked".
666            if policy.disposition == PolicyDisposition::Enforce {
667                result = CheckResult::Blocked
668            }
669        }
670        (result, violations)
671    }
672    /// https://www.w3.org/TR/CSP/#can-compile-wasm-bytes
673    pub fn is_wasm_evaluation_allowed(&self) -> (CheckResult, Vec<Violation>) {
674        let mut result = CheckResult::Allowed;
675        let mut violations = Vec::new();
676        // Step 3: For each policy of global’s CSP list:
677        for policy in &self.0 {
678            // Step 3.1: Let source-list be null.
679            let directive = policy
680                .directive_set
681                .iter()
682                // Step 3.2: If policy contains a directive whose name is "script-src",
683                // then set source-list to that directive’s value.
684                .find(|directive| directive.name == "script-src")
685                // Step 3.2: Otherwise if policy contains a directive whose name is "default-src",
686                // then set source-list to that directive’s value.
687                .or_else(|| {
688                    policy
689                        .directive_set
690                        .iter()
691                        .find(|directive| directive.name == "default-src")
692                });
693            let Some(directive) = directive else { continue };
694            let source_list = SourceList(&directive.value);
695            // Step 3.3: If source-list is non-null, and does not contain a source expression
696            // which is an ASCII case-insensitive match for the string "'unsafe-eval'",
697            // and does not contain a source expression which is an ASCII case-insensitive
698            // match for the string "'wasm-unsafe-eval'", then:
699            if source_list.does_a_source_list_allow_wasm_evaluation() == AllowResult::Allows {
700                continue;
701            }
702            // Step 3.3.1: Let violation be the result of executing § 2.4.1 Create a violation
703            // object for global, policy, and directive on global, policy, and "script-src".
704            violations.push(Violation {
705                // Step 5.3.5: Set violation’s resource to "wasm-eval".
706                resource: ViolationResource::WasmEval,
707                directive: directive.clone(),
708                policy: policy.clone(),
709            });
710            // Step 3.3.4: If policy’s disposition is "enforce", then set result to "Blocked".
711            if policy.disposition == PolicyDisposition::Enforce {
712                result = CheckResult::Blocked
713            }
714        }
715        (result, violations)
716    }
717    /// <https://w3c.github.io/webappsec-csp/#should-block-navigation-request>
718    ///
719    /// Here, `url_processor` is a callback to process trusted types (if applicable).
720    /// In case the Trusted Types algorithm returns an Error, return a None. Otherwise
721    /// return a Some with the string as provided by the policy.
722    ///
723    /// If trusted types are not applicable, then the `url_processor` can look like this:
724    /// ```rust
725    /// |s: &str| Some(s.to_owned());
726    /// ```
727    pub fn should_navigation_request_be_blocked<TrustedTypesUrlProcessor>(
728        &self,
729        request: &mut Request,
730        navigation_check_type: NavigationCheckType,
731        mut url_processor: TrustedTypesUrlProcessor,
732    ) -> (CheckResult, Vec<Violation>)
733    where
734        TrustedTypesUrlProcessor: FnMut(&str) -> Option<String>,
735    {
736        // Step 1: Let result be "Allowed".
737        let mut result = CheckResult::Allowed;
738        let mut violations = Vec::new();
739        // Step 2: For each policy of navigation request’s policy container’s CSP list:
740        for policy in &self.0 {
741            // Step 2.1: For each directive of policy:
742            for directive in &policy.directive_set {
743                // Step 2.1.1: If directive’s pre-navigation check returns "Allowed"
744                // when executed upon navigation request, type, and policy skip to the next directive.
745                if directive.pre_navigation_check(
746                    request,
747                    navigation_check_type,
748                    &mut url_processor,
749                    policy,
750                ) == CheckResult::Allowed
751                {
752                    continue;
753                }
754                // Step 2.1.2: Otherwise, let violation be the result of executing
755                // § 2.4.1 Create a violation object for global, policy, and directive
756                // on navigation request’s client’s global object, policy, and directive’s name.
757                violations.push(Violation {
758                    // Step 2.1.3: Set violation’s resource to navigation request’s URL.
759                    resource: ViolationResource::Url(request.url.clone()),
760                    directive: Directive {
761                        name: get_the_effective_directive_for_request(request).to_owned(),
762                        value: directive.value.clone(),
763                    },
764                    policy: policy.clone(),
765                });
766                // Step 2.1.5: If policy’s disposition is "enforce", then set result to "Blocked".
767                if policy.disposition == PolicyDisposition::Enforce {
768                    result = CheckResult::Blocked;
769                }
770            }
771        }
772        // Step 3: If result is "Allowed", and if navigation request’s current URL’s scheme is javascript:
773        if result == CheckResult::Allowed && request.current_url.scheme() == "javascript" {
774            // Step 3.1: For each policy of navigation request’s policy container’s CSP list:
775            for policy in &self.0 {
776                // Step 3.1.1: For each directive of policy:
777                for directive in &policy.directive_set {
778                    // Step 3.1.1.2: If directive’s inline check returns "Allowed" when executed upon null,
779                    // "navigation" and navigation request’s current URL, skip to the next directive.
780                    if directive.inline_check(
781                        &Element { nonce: None },
782                        InlineCheckType::Navigation,
783                        policy,
784                        request.current_url.as_str(),
785                    ) == CheckResult::Allowed
786                    {
787                        continue;
788                    }
789                    // Step 3.1.1.3: Otherwise, let violation be the result of executing
790                    // § 2.4.1 Create a violation object for global, policy, and directive
791                    // on navigation request’s client’s global object, policy, and directive’s name.
792                    violations.push(Violation {
793                        // Step 3.1.1.4: Set violation’s resource to navigation request’s URL.
794                        resource: ViolationResource::Inline { sample: None },
795                        directive: Directive {
796                            // Step 3.1.1.1: Let directive-name be the result of executing
797                            // § 6.8.2 Get the effective directive for inline checks on type.
798                            name: get_the_effective_directive_for_inline_checks(
799                                InlineCheckType::Navigation,
800                            )
801                            .to_owned(),
802                            value: directive.value.clone(),
803                        },
804                        policy: policy.clone(),
805                    });
806                    // Step 3.1.1.6: If policy’s disposition is "enforce", then set result to "Blocked".
807                    if policy.disposition == PolicyDisposition::Enforce {
808                        result = CheckResult::Blocked;
809                    }
810                }
811            }
812        }
813        (result, violations)
814    }
815    /// <https://w3c.github.io/webappsec-csp/#should-block-navigation-response>
816    pub fn should_navigation_response_to_navigation_request_be_blocked(
817        &self,
818        response: &Response,
819        self_origin: &Origin,
820        parent_navigable_origins: &Vec<Url>,
821    ) -> (CheckResult, Vec<Violation>) {
822        // Step 1. Let result be "Allowed".
823        let mut result = CheckResult::Allowed;
824        let mut violations = Vec::new();
825        // Step 2. For each policy of response CSP list’s policies:
826        for policy in &self.0 {
827            // Step 2.1. For each directive of policy:
828            for directive in &policy.directive_set {
829                // Step 2.1.1. If directive’s navigation response check returns "Allowed"
830                // when executed upon navigation request, type, navigation response, target,
831                // "response", policy, and response CSP list’s self-origin, skip to the next directive.
832                if directive.navigation_response_check(
833                    response,
834                    self_origin,
835                    parent_navigable_origins,
836                    policy,
837                ) == CheckResult::Allowed
838                {
839                    continue;
840                }
841                // Step 2.1.2. Otherwise, let violation be the result of executing
842                // § 2.4.1 Create a violation object for global, policy, and directive on null, policy, and directive’s name.
843                violations.push(Violation {
844                    // Step 2.1.3. Set violation’s resource to navigation response’s URL.
845                    resource: ViolationResource::Url(response.url.clone()),
846                    directive: directive.clone(),
847                    policy: policy.clone(),
848                });
849                // Step 2.1.5. If policy’s disposition is "enforce", then set result to "Blocked".
850                if policy.disposition == PolicyDisposition::Enforce {
851                    result = CheckResult::Blocked;
852                }
853            }
854        }
855        // Step 3. For each policy of navigation request’s policy container’s CSP list’s policies:
856        //
857        // Note: We do not implement this step, since there is no directive yet that requires it
858        (result, violations)
859    }
860}
861
862#[derive(Clone, Debug)]
863pub struct Element<'a> {
864    /// When there is no nonce, populate this member with `None`.
865    ///
866    /// When the element is not [nonceable], also populate it with `None`.
867    ///
868    /// [nonceable]: https://www.w3.org/TR/CSP/#is-element-nonceable
869    pub nonce: Option<Cow<'a, str>>,
870}
871
872/**
873The valid values for type are "script", "script attribute", "style", and "style attribute".
874
875https://www.w3.org/TR/CSP/#should-block-inline
876*/
877#[derive(Clone, Copy, Debug, Eq, PartialEq)]
878pub enum InlineCheckType {
879    Script,
880    ScriptAttribute,
881    Style,
882    StyleAttribute,
883    Navigation,
884}
885
886/**
887The valid values for type are "form-submission" and "other".
888
889https://w3c.github.io/webappsec-csp/#directive-pre-navigation-check
890*/
891#[derive(Clone, Copy, Debug, Eq, PartialEq)]
892pub enum NavigationCheckType {
893    FormSubmission,
894    Other,
895}
896
897/**
898request to be validated
899
900https://fetch.spec.whatwg.org/#concept-request
901*/
902#[derive(Clone, Debug)]
903pub struct Request {
904    pub url: Url,
905    pub current_url: Url,
906    pub origin: Origin,
907    pub redirect_count: u32,
908    pub destination: Destination,
909    pub initiator: Initiator,
910    pub nonce: String,
911    pub integrity_metadata: String,
912    pub parser_metadata: ParserMetadata,
913}
914
915#[derive(Clone, Copy, Debug, Eq, PartialEq)]
916pub enum ParserMetadata {
917    ParserInserted,
918    NotParserInserted,
919    None,
920}
921
922#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
923#[derive(Clone, Copy, Debug, Eq, PartialEq)]
924pub enum Initiator {
925    Download,
926    ImageSet,
927    Manifest,
928    Prefetch,
929    Prerender,
930    Fetch,
931    Xslt,
932    None,
933}
934
935#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
936#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
937pub enum Destination {
938    None,
939    Audio,
940    AudioWorklet,
941    Document,
942    Embed,
943    Font,
944    Frame,
945    IFrame,
946    Image,
947    Json,
948    Manifest,
949    Object,
950    PaintWorklet,
951    Report,
952    Script,
953    ServiceWorker,
954    SharedWorker,
955    Style,
956    Track,
957    Video,
958    WebIdentity,
959    Worker,
960    Xslt,
961}
962
963pub struct InvalidDestination;
964
965impl FromStr for Destination {
966    type Err = InvalidDestination;
967
968    fn from_str(s: &str) -> Result<Self, Self::Err> {
969        let destination = match s {
970            "" => Self::None,
971            "audio" => Self::Audio,
972            "audioworklet" => Self::AudioWorklet,
973            "document" => Self::Document,
974            "embed" => Self::Embed,
975            "font" => Self::Font,
976            "frame" => Self::Frame,
977            "iframe" => Self::IFrame,
978            "image" => Self::Image,
979            "json" => Self::Json,
980            "manifest" => Self::Manifest,
981            "object" => Self::Object,
982            "paintworklet" => Self::PaintWorklet,
983            "report" => Self::Report,
984            "script" => Self::Script,
985            "serviceworker" => Self::ServiceWorker,
986            "sharedworker" => Self::SharedWorker,
987            "style" => Self::Style,
988            "track" => Self::Track,
989            "video" => Self::Video,
990            "webidentity" => Self::WebIdentity,
991            "worker" => Self::Worker,
992            "xslt" => Self::Xslt,
993            _ => return Err(InvalidDestination),
994        };
995
996        Ok(destination)
997    }
998}
999
1000impl Destination {
1001    /// https://fetch.spec.whatwg.org/#request-destination-script-like
1002    pub fn is_script_like(self) -> bool {
1003        use Destination::*;
1004        matches!(
1005            self,
1006            AudioWorklet | PaintWorklet | Script | ServiceWorker | SharedWorker | Worker | Xslt
1007        )
1008    }
1009
1010    pub const fn as_str(&self) -> &'static str {
1011        match self {
1012            Self::None => "",
1013            Self::Audio => "audio",
1014            Self::AudioWorklet => "audioworklet",
1015            Self::Document => "document",
1016            Self::Embed => "embed",
1017            Self::Font => "font",
1018            Self::Frame => "frame",
1019            Self::IFrame => "iframe",
1020            Self::Image => "image",
1021            Self::Json => "json",
1022            Self::Manifest => "manifest",
1023            Self::Object => "object",
1024            Self::PaintWorklet => "paintworklet",
1025            Self::Report => "report",
1026            Self::Script => "script",
1027            Self::ServiceWorker => "serviceworker",
1028            Self::SharedWorker => "sharedworker",
1029            Self::Style => "style",
1030            Self::Track => "track",
1031            Self::Video => "video",
1032            Self::WebIdentity => "webidentity",
1033            Self::Worker => "worker",
1034            Self::Xslt => "xslt",
1035        }
1036    }
1037}
1038
1039/**
1040response to be validated
1041https://fetch.spec.whatwg.org/#concept-response
1042*/
1043#[derive(Clone, Debug)]
1044pub struct Response {
1045    pub url: Url,
1046    pub redirect_count: u32,
1047}
1048
1049/// <https://fetch.spec.whatwg.org/#is-local>
1050fn is_local_url(url: &Url) -> bool {
1051    // > A URL is local if its scheme is a local scheme.
1052    let scheme = url.scheme();
1053    // > A local scheme is "about", "blob", or "data".
1054    scheme == "about" || scheme == "blob" || scheme == "data"
1055}
1056
1057/**
1058violation information
1059
1060https://www.w3.org/TR/CSP/#violation
1061*/
1062#[derive(Clone, Debug)]
1063#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1064pub struct Violation {
1065    pub resource: ViolationResource,
1066    pub directive: Directive,
1067    pub policy: Policy,
1068}
1069
1070/**
1071violation information
1072
1073https://www.w3.org/TR/CSP/#violation
1074*/
1075#[derive(Clone, Debug, Eq, PartialEq)]
1076#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1077pub enum ViolationResource {
1078    Url(Url),
1079    Inline { sample: Option<String> },
1080    TrustedTypePolicy { sample: String },
1081    TrustedTypeSink { sample: String },
1082    Eval { sample: Option<String> },
1083    WasmEval,
1084}
1085
1086/**
1087Many algorithms are allowed to return either "Allowed" or "Blocked".
1088The spec describes these as strings.
1089*/
1090#[derive(Clone, Debug, Eq, PartialEq)]
1091#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1092pub enum CheckResult {
1093    Allowed,
1094    Blocked,
1095}
1096
1097/**
1098https://www.w3.org/TR/CSP/#does-request-violate-policy
1099*/
1100#[derive(Clone, Debug, Eq, PartialEq)]
1101#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1102pub enum Violates {
1103    DoesNotViolate,
1104    Directive(Directive),
1105}
1106
1107/// https://www.w3.org/TR/CSP/#policy-disposition
1108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1109#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1110pub enum PolicyDisposition {
1111    Enforce,
1112    Report,
1113}
1114
1115/// https://www.w3.org/TR/CSP/#policy-source
1116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1117#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1118pub enum PolicySource {
1119    Header,
1120    Meta,
1121}
1122
1123/// https://www.w3.org/TR/CSP/#directives
1124#[derive(Clone, Debug, Eq, PartialEq)]
1125#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1126pub struct Directive {
1127    pub name: String,
1128    pub value: Vec<String>,
1129}
1130
1131impl Display for Directive {
1132    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
1133        <str as Display>::fmt(&self.name[..], f)?;
1134        write!(f, " ")?;
1135        for (i, token) in self.value.iter().enumerate() {
1136            if i != 0 {
1137                write!(f, " ")?;
1138            }
1139            <str as Display>::fmt(&token[..], f)?;
1140        }
1141        Ok(())
1142    }
1143}
1144
1145impl Directive {
1146    /// https://www.w3.org/TR/CSP/#serialized-directive
1147    pub fn is_valid(&self) -> bool {
1148        DIRECTIVE_NAME_GRAMMAR.is_match(&self.name)
1149            && self
1150                .value
1151                .iter()
1152                .all(|t| DIRECTIVE_VALUE_TOKEN_GRAMMAR.is_match(&t[..]))
1153    }
1154    /// https://www.w3.org/TR/CSP/#directive-pre-request-check
1155    pub fn pre_request_check(&self, request: &Request, policy: &Policy) -> CheckResult {
1156        use CheckResult::*;
1157        match &self.name[..] {
1158            "child-src" => {
1159                let name = get_the_effective_directive_for_request(request);
1160                if !should_fetch_directive_execute(name, "child-src", policy) {
1161                    return Allowed;
1162                }
1163                (Directive {
1164                    name: String::from(name),
1165                    value: self.value.clone(),
1166                })
1167                .pre_request_check(request, policy)
1168            }
1169            "connect-src" => {
1170                let name = get_the_effective_directive_for_request(request);
1171                if !should_fetch_directive_execute(name, "connect-src", policy) {
1172                    return Allowed;
1173                }
1174                if SourceList(&self.value[..]).does_request_match_source_list(request)
1175                    == DoesNotMatch
1176                {
1177                    return Blocked;
1178                }
1179                Allowed
1180            }
1181            "default-src" => {
1182                let name = get_the_effective_directive_for_request(request);
1183                if !should_fetch_directive_execute(name, "default-src", policy) {
1184                    return Allowed;
1185                }
1186                (Directive {
1187                    name: String::from(name),
1188                    value: self.value.clone(),
1189                })
1190                .pre_request_check(request, policy)
1191            }
1192            "font-src" => {
1193                let name = get_the_effective_directive_for_request(request);
1194                if !should_fetch_directive_execute(name, "font-src", policy) {
1195                    return Allowed;
1196                }
1197                if SourceList(&self.value[..]).does_request_match_source_list(request)
1198                    == DoesNotMatch
1199                {
1200                    return Blocked;
1201                }
1202                Allowed
1203            }
1204            "frame-src" => {
1205                let name = get_the_effective_directive_for_request(request);
1206                if !should_fetch_directive_execute(name, "frame-src", policy) {
1207                    return Allowed;
1208                }
1209                if SourceList(&self.value[..]).does_request_match_source_list(request)
1210                    == DoesNotMatch
1211                {
1212                    return Blocked;
1213                }
1214                Allowed
1215            }
1216            "img-src" => {
1217                let name = get_the_effective_directive_for_request(request);
1218                if !should_fetch_directive_execute(name, "img-src", policy) {
1219                    return Allowed;
1220                }
1221                if SourceList(&self.value[..]).does_request_match_source_list(request)
1222                    == DoesNotMatch
1223                {
1224                    return Blocked;
1225                }
1226                Allowed
1227            }
1228            "manifest-src" => {
1229                let name = get_the_effective_directive_for_request(request);
1230                if !should_fetch_directive_execute(name, "manifest-src", policy) {
1231                    return Allowed;
1232                }
1233                if SourceList(&self.value[..]).does_request_match_source_list(request)
1234                    == DoesNotMatch
1235                {
1236                    return Blocked;
1237                }
1238                Allowed
1239            }
1240            "media-src" => {
1241                let name = get_the_effective_directive_for_request(request);
1242                if !should_fetch_directive_execute(name, "media-src", policy) {
1243                    return Allowed;
1244                }
1245                if SourceList(&self.value[..]).does_request_match_source_list(request)
1246                    == DoesNotMatch
1247                {
1248                    return Blocked;
1249                }
1250                Allowed
1251            }
1252            "object-src" => {
1253                let name = get_the_effective_directive_for_request(request);
1254                if !should_fetch_directive_execute(name, "object-src", policy) {
1255                    return Allowed;
1256                }
1257                if SourceList(&self.value[..]).does_request_match_source_list(request)
1258                    == DoesNotMatch
1259                {
1260                    return Blocked;
1261                }
1262                Allowed
1263            }
1264            "script-src" => {
1265                let name = get_the_effective_directive_for_request(request);
1266                if !should_fetch_directive_execute(name, "script-src", policy) {
1267                    return Allowed;
1268                }
1269                script_directives_prerequest_check(request, self)
1270            }
1271            "script-src-elem" => {
1272                let name = get_the_effective_directive_for_request(request);
1273                if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1274                    return Allowed;
1275                }
1276                script_directives_prerequest_check(request, self)
1277            }
1278            "style-src" => {
1279                let name = get_the_effective_directive_for_request(request);
1280                if !should_fetch_directive_execute(name, "style-src", 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_request_match_source_list(request) == DoesNotMatch {
1288                    return Blocked;
1289                }
1290                Allowed
1291            }
1292            "style-src-elem" => {
1293                let name = get_the_effective_directive_for_request(request);
1294                if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1295                    return Allowed;
1296                }
1297                let source_list = SourceList(&self.value);
1298                if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1299                    return Allowed;
1300                }
1301                if source_list.does_request_match_source_list(request) == DoesNotMatch {
1302                    return Blocked;
1303                }
1304                Allowed
1305            }
1306            "worker-src" => {
1307                let name = get_the_effective_directive_for_request(request);
1308                if !should_fetch_directive_execute(name, "worker-src", policy) {
1309                    return Allowed;
1310                }
1311                let source_list = SourceList(&self.value);
1312                if source_list.does_request_match_source_list(request) == DoesNotMatch {
1313                    return Blocked;
1314                }
1315                Allowed
1316            }
1317            _ => Allowed,
1318        }
1319    }
1320    /// https://www.w3.org/TR/CSP/#directive-post-request-check
1321    pub fn post_request_check(
1322        &self,
1323        request: &Request,
1324        response: &Response,
1325        policy: &Policy,
1326    ) -> CheckResult {
1327        use CheckResult::*;
1328        match &self.name[..] {
1329            "child-src" => {
1330                let name = get_the_effective_directive_for_request(request);
1331                if !should_fetch_directive_execute(name, "child-src", policy) {
1332                    return Allowed;
1333                }
1334                Directive {
1335                    name: name.to_owned(),
1336                    value: self.value.clone(),
1337                }
1338                .post_request_check(request, response, policy)
1339            }
1340            "connect-src" => {
1341                let name = get_the_effective_directive_for_request(request);
1342                if !should_fetch_directive_execute(name, "connect-src", policy) {
1343                    return Allowed;
1344                }
1345                let source_list = SourceList(&self.value);
1346                if source_list.does_response_to_request_match_source_list(request, response)
1347                    == DoesNotMatch
1348                {
1349                    return Blocked;
1350                }
1351                Allowed
1352            }
1353            "default-src" => {
1354                let name = get_the_effective_directive_for_request(request);
1355                if !should_fetch_directive_execute(name, "default-src", policy) {
1356                    return Allowed;
1357                }
1358                Directive {
1359                    name: name.to_owned(),
1360                    value: self.value.clone(),
1361                }
1362                .post_request_check(request, response, policy)
1363            }
1364            "font-src" => {
1365                let name = get_the_effective_directive_for_request(request);
1366                if !should_fetch_directive_execute(name, "font-src", policy) {
1367                    return Allowed;
1368                }
1369                let source_list = SourceList(&self.value);
1370                if source_list.does_response_to_request_match_source_list(request, response)
1371                    == DoesNotMatch
1372                {
1373                    return Blocked;
1374                }
1375                Allowed
1376            }
1377            "frame-src" => {
1378                let name = get_the_effective_directive_for_request(request);
1379                if !should_fetch_directive_execute(name, "frame-src", policy) {
1380                    return Allowed;
1381                }
1382                let source_list = SourceList(&self.value);
1383                if source_list.does_response_to_request_match_source_list(request, response)
1384                    == DoesNotMatch
1385                {
1386                    return Blocked;
1387                }
1388                Allowed
1389            }
1390            "img-src" => {
1391                let name = get_the_effective_directive_for_request(request);
1392                if !should_fetch_directive_execute(name, "img-src", policy) {
1393                    return Allowed;
1394                }
1395                let source_list = SourceList(&self.value);
1396                if source_list.does_response_to_request_match_source_list(request, response)
1397                    == DoesNotMatch
1398                {
1399                    return Blocked;
1400                }
1401                Allowed
1402            }
1403            "manifest-src" => {
1404                let name = get_the_effective_directive_for_request(request);
1405                if !should_fetch_directive_execute(name, "manifest-src", policy) {
1406                    return Allowed;
1407                }
1408                let source_list = SourceList(&self.value);
1409                if source_list.does_response_to_request_match_source_list(request, response)
1410                    == DoesNotMatch
1411                {
1412                    return Blocked;
1413                }
1414                Allowed
1415            }
1416            "media-src" => {
1417                let name = get_the_effective_directive_for_request(request);
1418                if !should_fetch_directive_execute(name, "media-src", policy) {
1419                    return Allowed;
1420                }
1421                let source_list = SourceList(&self.value);
1422                if source_list.does_response_to_request_match_source_list(request, response)
1423                    == DoesNotMatch
1424                {
1425                    return Blocked;
1426                }
1427                Allowed
1428            }
1429            "object-src" => {
1430                let name = get_the_effective_directive_for_request(request);
1431                if !should_fetch_directive_execute(name, "object-src", policy) {
1432                    return Allowed;
1433                }
1434                let source_list = SourceList(&self.value);
1435                if source_list.does_response_to_request_match_source_list(request, response)
1436                    == DoesNotMatch
1437                {
1438                    return Blocked;
1439                }
1440                Allowed
1441            }
1442            "script-src" => {
1443                let name = get_the_effective_directive_for_request(request);
1444                if !should_fetch_directive_execute(name, "script-src", policy) {
1445                    return Allowed;
1446                }
1447                script_directives_postrequest_check(request, response, self)
1448            }
1449            "script-src-elem" => {
1450                let name = get_the_effective_directive_for_request(request);
1451                if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1452                    return Allowed;
1453                }
1454                script_directives_postrequest_check(request, response, self)
1455            }
1456            "style-src" => {
1457                let name = get_the_effective_directive_for_request(request);
1458                if !should_fetch_directive_execute(name, "style-src", policy) {
1459                    return Allowed;
1460                }
1461                let source_list = SourceList(&self.value);
1462                if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1463                    return Allowed;
1464                }
1465                if source_list.does_response_to_request_match_source_list(request, response)
1466                    == DoesNotMatch
1467                {
1468                    return Blocked;
1469                }
1470                Allowed
1471            }
1472            "style-src-elem" => {
1473                let name = get_the_effective_directive_for_request(request);
1474                if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1475                    return Allowed;
1476                }
1477                let source_list = SourceList(&self.value);
1478                if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1479                    return Allowed;
1480                }
1481                if source_list.does_response_to_request_match_source_list(request, response)
1482                    == DoesNotMatch
1483                {
1484                    return Blocked;
1485                }
1486                Allowed
1487            }
1488            "worker-src" => {
1489                let name = get_the_effective_directive_for_request(request);
1490                if !should_fetch_directive_execute(name, "worker-src", policy) {
1491                    return Allowed;
1492                }
1493                let source_list = SourceList(&self.value);
1494                if source_list.does_response_to_request_match_source_list(request, response)
1495                    == DoesNotMatch
1496                {
1497                    return Blocked;
1498                }
1499                Allowed
1500            }
1501            _ => Allowed,
1502        }
1503    }
1504    /// https://www.w3.org/TR/CSP/#directive-inline-check
1505    pub fn inline_check(
1506        &self,
1507        element: &Element,
1508        type_: InlineCheckType,
1509        policy: &Policy,
1510        source: &str,
1511    ) -> CheckResult {
1512        use CheckResult::*;
1513        match &self.name[..] {
1514            "default-src" => {
1515                let name = get_the_effective_directive_for_inline_checks(type_);
1516                if !should_fetch_directive_execute(name, "default-src", policy) {
1517                    return Allowed;
1518                }
1519                Directive {
1520                    name: name.to_owned(),
1521                    value: self.value.clone(),
1522                }
1523                .inline_check(element, type_, policy, source)
1524            }
1525            "script-src" => {
1526                let name = get_the_effective_directive_for_inline_checks(type_);
1527                if !should_fetch_directive_execute(name, "script-src", policy) {
1528                    return Allowed;
1529                }
1530                let source_list = SourceList(&self.value);
1531                if source_list
1532                    .does_element_match_source_list_for_type_and_source(element, type_, source)
1533                    == DoesNotMatch
1534                {
1535                    return Blocked;
1536                }
1537                Allowed
1538            }
1539            "script-src-elem" => {
1540                let name = get_the_effective_directive_for_inline_checks(type_);
1541                if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1542                    return Allowed;
1543                }
1544                let source_list = SourceList(&self.value);
1545                if source_list
1546                    .does_element_match_source_list_for_type_and_source(element, type_, source)
1547                    == DoesNotMatch
1548                {
1549                    return Blocked;
1550                }
1551                Allowed
1552            }
1553            "script-src-attr" => {
1554                let name = get_the_effective_directive_for_inline_checks(type_);
1555                if !should_fetch_directive_execute(name, "script-src-attr", policy) {
1556                    return Allowed;
1557                }
1558                let source_list = SourceList(&self.value);
1559                if source_list
1560                    .does_element_match_source_list_for_type_and_source(element, type_, source)
1561                    == DoesNotMatch
1562                {
1563                    return Blocked;
1564                }
1565                Allowed
1566            }
1567            "style-src" => {
1568                let name = get_the_effective_directive_for_inline_checks(type_);
1569                if !should_fetch_directive_execute(name, "style-src", policy) {
1570                    return Allowed;
1571                }
1572                let source_list = SourceList(&self.value);
1573                if source_list
1574                    .does_element_match_source_list_for_type_and_source(element, type_, source)
1575                    == DoesNotMatch
1576                {
1577                    return Blocked;
1578                }
1579                Allowed
1580            }
1581            "style-src-elem" => {
1582                let name = get_the_effective_directive_for_inline_checks(type_);
1583                if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1584                    return Allowed;
1585                }
1586                let source_list = SourceList(&self.value);
1587                if source_list
1588                    .does_element_match_source_list_for_type_and_source(element, type_, source)
1589                    == DoesNotMatch
1590                {
1591                    return Blocked;
1592                }
1593                Allowed
1594            }
1595            "style-src-attr" => {
1596                let name = get_the_effective_directive_for_inline_checks(type_);
1597                if !should_fetch_directive_execute(name, "style-src-attr", policy) {
1598                    return Allowed;
1599                }
1600                let source_list = SourceList(&self.value);
1601                if source_list
1602                    .does_element_match_source_list_for_type_and_source(element, type_, source)
1603                    == DoesNotMatch
1604                {
1605                    return Blocked;
1606                }
1607                Allowed
1608            }
1609            _ => Allowed,
1610        }
1611    }
1612    /// <https://html.spec.whatwg.org/multipage/#csp-derived-sandboxing-flags>
1613    pub fn get_sandboxing_flag_set_for_document(
1614        &self,
1615        policy: &Policy,
1616    ) -> Option<SandboxingFlagSet> {
1617        debug_assert!(&self.name[..] == "sandbox");
1618        // Step 2.1. If policy's disposition is not "enforce", then continue.
1619        if policy.disposition != PolicyDisposition::Enforce {
1620            None
1621        } else {
1622            // Step 5. Return the result of parsing the sandboxing directive directive.
1623            Some(parse_a_sandboxing_directive(&self.value[..]))
1624        }
1625    }
1626    /// <https://w3c.github.io/webappsec-csp/#directive-pre-navigation-check>
1627    pub fn pre_navigation_check<TrustedTypesUrlProcessor>(
1628        &self,
1629        request: &mut Request,
1630        type_: NavigationCheckType,
1631        mut url_processor: TrustedTypesUrlProcessor,
1632        _policy: &Policy,
1633    ) -> CheckResult
1634    where
1635        TrustedTypesUrlProcessor: FnMut(&str) -> Option<String>,
1636    {
1637        use CheckResult::*;
1638        match &self.name[..] {
1639            // <https://w3c.github.io/webappsec-csp/#form-action-pre-navigate>
1640            "form-action" => {
1641                // Step 2: If navigation type is "form-submission":
1642                if type_ == NavigationCheckType::FormSubmission {
1643                    let source_list = SourceList(&self.value);
1644                    // Step 2.1: If the result of executing § 6.7.2.5 Does request match source list? on request,
1645                    // this directive’s value, and a policy, is "Does Not Match", return "Blocked".
1646                    if source_list.does_request_match_source_list(request) == DoesNotMatch {
1647                        return Blocked;
1648                    }
1649                }
1650                // Step 3: Return "Allowed".
1651                Allowed
1652            }
1653            // <https://www.w3.org/TR/trusted-types/#require-trusted-types-for-pre-navigation-check>
1654            "require-trusted-types-for" => {
1655                let url = &request.url;
1656                // Step 1. If request’s url’s scheme is not "javascript", return "Allowed" and abort further steps.
1657                if url.scheme() != "javascript" {
1658                    return Allowed;
1659                }
1660                // Step 2. Let urlString be the result of running the URL serializer on request’s url.
1661                //
1662                // Already done when creating Request
1663                // Step 3. Let encodedScriptSource be the result of removing the leading "javascript:" from urlString.
1664                let encoded_script_source = &url[Position::AfterScheme..][1..];
1665                // Step 4. Let convertedScriptSource be the result of executing Process value with a default policy algorithm
1666                // If that algorithm threw an error or convertedScriptSource is not a TrustedScript object,
1667                // return "Blocked" and abort further steps.
1668                let Some(converted_script_source) = url_processor(encoded_script_source) else {
1669                    return Blocked;
1670                };
1671                // Step 5. Set urlString to be the result of prepending "javascript:" to stringified convertedScriptSource.
1672                let url_string = "javascript:".to_owned() + &converted_script_source;
1673                // Step 6. Let newURL be the result of running the URL parser on urlString.
1674                // If the parser returns a failure, return "Blocked" and abort further steps.
1675                let Ok(new_url) = Url::parse(&url_string) else {
1676                    return Blocked;
1677                };
1678                // Step 7. Set request’s url to newURL.
1679                request.url = new_url;
1680                // Step 8. Return "Allowed".
1681                Allowed
1682            }
1683            _ => Allowed,
1684        }
1685    }
1686
1687    pub fn navigation_response_check(
1688        &self,
1689        response: &Response,
1690        self_origin: &Origin,
1691        parent_navigable_origins: &Vec<Url>,
1692        _policy: &Policy,
1693    ) -> CheckResult {
1694        use CheckResult::*;
1695        match &self.name[..] {
1696            // <https://w3c.github.io/webappsec-csp/#frame-ancestors-navigation-response>
1697            "frame-ancestors" => {
1698                // Step 1. If navigation response’s URL is local, return "Allowed".
1699                if is_local_url(&response.url) {
1700                    return Allowed;
1701                }
1702                // Step 2. Assert: request, navigation response, and navigation type,
1703                // are unused from this point forward in this algorithm,
1704                // as frame-ancestors is concerned only with navigation response’s frame-ancestors directive.
1705
1706                // Step 3. If check type is "source", return "Allowed".
1707                //
1708                // We only call this once for responses
1709
1710                let source_list = SourceList(&self.value);
1711                // Step 4. If target is not a child navigable, return "Allowed".
1712                // Step 5. Let current be target.
1713                // Step 6. While current is a child navigable:
1714                for origin in parent_navigable_origins {
1715                    // Step 6.1. Let document be current’s container document.
1716                    // Step 6.2. Let origin be the result of executing the URL parser on the ASCII serialization of document’s origin.
1717                    // Step 6.3. If § 6.7.2.7 Does url match source list in origin with redirect count? returns
1718                    // Does Not Match when executed upon origin, this directive’s value, self-origin, and 0, return "Blocked".
1719                    if source_list.does_url_match_source_list_in_origin_with_redirect_count(
1720                        origin,
1721                        self_origin,
1722                        0,
1723                    ) == DoesNotMatch
1724                    {
1725                        return Blocked;
1726                    }
1727                    // Step 6.4. Set current to document’s node navigable.
1728                }
1729                // Step 7. Return "Allowed".
1730                Allowed
1731            }
1732            _ => Allowed,
1733        }
1734    }
1735}
1736
1737/// https://www.w3.org/TR/CSP/#effective-directive-for-inline-check
1738fn get_the_effective_directive_for_inline_checks(type_: InlineCheckType) -> &'static str {
1739    use InlineCheckType::*;
1740    match type_ {
1741        Script | Navigation => "script-src-elem",
1742        ScriptAttribute => "script-src-attr",
1743        Style => "style-src-elem",
1744        StyleAttribute => "style-src-attr",
1745    }
1746}
1747
1748/// <https://www.w3.org/TR/CSP/#script-pre-request>
1749fn script_directives_prerequest_check(request: &Request, directive: &Directive) -> CheckResult {
1750    use CheckResult::*;
1751    // Step 1. If request’s destination is script-like:
1752    if request_is_script_like(request) {
1753        let source_list = SourceList(&directive.value[..]);
1754        // Step 1.1. If the result of executing § 6.7.2.3 Does nonce match source list? on
1755        // request’s cryptographic nonce metadata and this directive’s value is "Matches", return "Allowed".
1756        if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1757            return Allowed;
1758        }
1759        // Step 1.2. If the result of executing § 6.7.2.4 Does integrity metadata match source list? on
1760        // request’s integrity metadata and this directive’s value is "Matches", return "Allowed".
1761        if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata)
1762            == Matches
1763        {
1764            return Allowed;
1765        }
1766        // Step 1.3. If directive’s value contains a source expression that is an
1767        // ASCII case-insensitive match for the "'strict-dynamic'" keyword-source:
1768        if directive
1769            .value
1770            .iter()
1771            .any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'"))
1772        {
1773            // Step 1.3.1. If the request’s parser metadata is "parser-inserted", return "Blocked".
1774            if request.parser_metadata == ParserMetadata::ParserInserted {
1775                return Blocked;
1776            }
1777            // Otherwise, return "Allowed".
1778            return Allowed;
1779        }
1780
1781        // Step 1.4. If the result of executing § 6.7.2.5 Does request match source list? on
1782        // request, directive’s value, and policy, is "Does Not Match", return "Blocked".
1783        if source_list.does_request_match_source_list(request) == DoesNotMatch {
1784            return Blocked;
1785        }
1786    }
1787    // Step 2. Return "Allowed".
1788    Allowed
1789}
1790
1791/// https://www.w3.org/TR/CSP/#script-post-request
1792fn script_directives_postrequest_check(
1793    request: &Request,
1794    response: &Response,
1795    directive: &Directive,
1796) -> CheckResult {
1797    use CheckResult::*;
1798    // Step 1. If request’s destination is script-like:
1799    if request_is_script_like(request) {
1800        // Step 1.1. Call potentially report hash with response, request, directive and policy.
1801        // TODO
1802        let source_list = SourceList(&directive.value[..]);
1803        // Step 1.2. If the result of executing § 6.7.2.3 Does nonce match source list? on
1804        // request’s cryptographic nonce metadata and this directive’s value is "Matches", return "Allowed".
1805        if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1806            return Allowed;
1807        }
1808        // Step 1.3. If the result of executing § 6.7.2.4 Does integrity metadata match source list? on
1809        // request’s integrity metadata and this directive’s value is "Matches", return "Allowed".
1810        if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata)
1811            == Matches
1812        {
1813            return Allowed;
1814        }
1815        // Step 1.4. If directive’s value contains "'strict-dynamic'":
1816        if directive
1817            .value
1818            .iter()
1819            .any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'"))
1820        {
1821            // Step 1.4.1. If the request’s parser metadata is "parser-inserted", return "Blocked".
1822            if request.parser_metadata == ParserMetadata::ParserInserted {
1823                return Blocked;
1824            }
1825            // Otherwise, return "Allowed".
1826            return Allowed;
1827        }
1828        // Step 1.5. If the result of executing § 6.7.2.6 Does response to request match source list? on
1829        // response, request, directive’s value, and policy, is "Does Not Match", return "Blocked".
1830        if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch
1831        {
1832            return Blocked;
1833        }
1834    }
1835    // Step 2. Return "Allowed".
1836    Allowed
1837}
1838
1839/// https://fetch.spec.whatwg.org/#request-destination-script-like
1840fn request_is_script_like(request: &Request) -> bool {
1841    request.destination.is_script_like()
1842}
1843
1844/// https://www.w3.org/TR/CSP/#should-directive-execute
1845fn should_fetch_directive_execute(
1846    effective_directive_name: &str,
1847    directive_name: &str,
1848    policy: &Policy,
1849) -> bool {
1850    let directive_fallback_list = get_fetch_directive_fallback_list(effective_directive_name);
1851    for fallback_directive in directive_fallback_list {
1852        if directive_name == *fallback_directive {
1853            return true;
1854        }
1855        if policy.contains_a_directive_whose_name_is(fallback_directive) {
1856            return false;
1857        }
1858    }
1859    false
1860}
1861
1862/// https://www.w3.org/TR/CSP/#directive-fallback-list
1863fn get_fetch_directive_fallback_list(directive_name: &str) -> &'static [&'static str] {
1864    match directive_name {
1865        "script-src-elem" => &["script-src-elem", "script-src", "default-src"],
1866        "script-src-attr" => &["script-src-attr", "script-src", "default-src"],
1867        "style-src-elem" => &["style-src-elem", "style-src", "default-src"],
1868        "style-src-attr" => &["style-src-attr", "style-src", "default-src"],
1869        "worker-src" => &["worker-src", "child-src", "script-src", "default-src"],
1870        "connect-src" => &["connect-src", "default-src"],
1871        "manifest-src" => &["manifest-src", "default-src"],
1872        "object-src" => &["object-src", "default-src"],
1873        "frame-src" => &["frame-src", "child-src", "default-src"],
1874        "media-src" => &["media-src", "default-src"],
1875        "font-src" => &["font-src", "default-src"],
1876        "img-src" => &["img-src", "default-src"],
1877        _ => &[],
1878    }
1879}
1880
1881/// https://www.w3.org/TR/CSP/#effective-directive-for-a-request
1882fn get_the_effective_directive_for_request(request: &Request) -> &'static str {
1883    use Destination::*;
1884    use Initiator::*;
1885    // Step 1: If request’s initiator is "prefetch" or "prerender", return default-src.
1886    if request.initiator == Prefetch || request.initiator == Prerender {
1887        return "default-src";
1888    }
1889    // Step 2: Switch on request’s destination, and execute the associated steps:
1890    match request.destination {
1891        Destination::Manifest => "manifest-src",
1892        Object | Embed => "object-src",
1893        Frame | IFrame => "frame-src",
1894        Audio | Track | Video => "media-src",
1895        Font => "font-src",
1896        Image => "img-src",
1897        Style => "style-src-elem",
1898        Script | Destination::Xslt | AudioWorklet | PaintWorklet => "script-src-elem",
1899        ServiceWorker | SharedWorker | Worker => "worker-src",
1900        Json | WebIdentity => "connect-src",
1901        Report => "",
1902        // Step 3: Return connect-src.
1903        _ => "connect-src",
1904    }
1905}
1906
1907/// https://www.w3.org/TR/CSP/#match-element-to-source-list
1908#[derive(Clone, Debug, Eq, PartialEq)]
1909pub enum MatchResult {
1910    Matches,
1911    DoesNotMatch,
1912}
1913
1914/// https://www.w3.org/TR/CSP/#grammardef-directive-name
1915static DIRECTIVE_NAME_GRAMMAR: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^[0-9a-z\-]+$"#).unwrap());
1916/// https://www.w3.org/TR/CSP/#grammardef-directive-value
1917static DIRECTIVE_VALUE_TOKEN_GRAMMAR: Lazy<Regex> =
1918    Lazy::new(|| Regex::new(r#"^[\u{21}-\u{2B}\u{2D}-\u{3A}\u{3C}-\u{7E}]+$"#).unwrap());
1919/// https://www.w3.org/TR/CSP/#grammardef-nonce-source
1920static NONCE_SOURCE_GRAMMAR: Lazy<Regex> =
1921    Lazy::new(|| Regex::new(r#"^'nonce-(?P<n>[a-zA-Z0-9\+/\-_]+=*)'$"#).unwrap());
1922static NONE_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^'none'$"#).unwrap());
1923/// https://www.w3.org/TR/CSP/#grammardef-scheme-source
1924static SCHEME_SOURCE_GRAMMAR: Lazy<Regex> =
1925    Lazy::new(|| Regex::new(r#"^(?P<scheme>[a-zA-Z][a-zA-Z0-9\+\-\.]*):$"#).unwrap());
1926/// https://www.w3.org/TR/CSP/#grammardef-host-source
1927static HOST_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
1928    // host-part   = "*" / [ "*." ] 1*host-char *( "." 1*host-char ) [ "." ]
1929    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()
1930});
1931/// https://www.w3.org/TR/CSP/#grammardef-hash-source
1932static HASH_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
1933    Regex::new(r#"^'(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)'$"#)
1934        .unwrap()
1935});
1936
1937/// https://www.w3.org/TR/CSP/#framework-directive-source-list
1938#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1939struct SourceList<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item = &'a U>>(I);
1940
1941impl<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item = &'a U>> SourceList<'a, U, I> {
1942    /// https://www.w3.org/TR/CSP/#match-nonce-to-source-list
1943    fn does_nonce_match_source_list(&self, nonce: &str) -> MatchResult {
1944        if nonce.is_empty() {
1945            return DoesNotMatch;
1946        };
1947        for expression in self.0.clone().into_iter() {
1948            if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression.borrow()) {
1949                if let Some(captured_nonce) = captures.name("n") {
1950                    if nonce == captured_nonce.as_str() {
1951                        return Matches;
1952                    }
1953                }
1954            }
1955        }
1956        DoesNotMatch
1957    }
1958    /// https://www.w3.org/TR/CSP/#match-integrity-metadata-to-source-list
1959    fn does_integrity_metadata_match_source_list(&self, integrity_metadata: &str) -> MatchResult {
1960        // Step 2: Let integrity expressions be the set of source expressions in source list that match the hash-source grammar.
1961        let integrity_expressions: Vec<HashFunction> = self
1962            .0
1963            .clone()
1964            .into_iter()
1965            .filter_map(|expression| {
1966                if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression.borrow()) {
1967                    if let (Some(algorithm), Some(value)) = (
1968                        captures
1969                            .name("algorithm")
1970                            .and_then(|a| HashAlgorithm::from_name(a.as_str())),
1971                        captures.name("value"),
1972                    ) {
1973                        return Some(HashFunction {
1974                            algorithm,
1975                            value: String::from(value.as_str()),
1976                        });
1977                    }
1978                }
1979                None
1980            })
1981            .collect();
1982        // Step 3: If integrity expressions is empty, return "Does Not Match".
1983        if integrity_expressions.is_empty() {
1984            return DoesNotMatch;
1985        }
1986        // Step 4: Let integrity sources be the result of executing the algorithm defined in SRI § 3.3.3 Parse metadata. on integrity metadata.
1987        let integrity_sources = parse_subresource_integrity_metadata(integrity_metadata);
1988        match integrity_sources {
1989            // Step 5: If integrity sources is "no metadata" or an empty set, return "Does Not Match".
1990            SubresourceIntegrityMetadata::NoMetadata => DoesNotMatch,
1991            SubresourceIntegrityMetadata::IntegritySources(integrity_sources) => {
1992                if integrity_sources.is_empty() {
1993                    return DoesNotMatch;
1994                }
1995                // Step 6: For each source of integrity sources:
1996                for source in &integrity_sources {
1997                    // Step 6.1: If integrity expressions does not contain a source expression whose hash-algorithm
1998                    // is an ASCII case-insensitive match for source’s hash-algorithm,
1999                    // and whose base64-value is identical to source’s base64-value, return "Does Not Match".
2000                    //
2001                    // Note that the case-insensitivy is already handled in HashAlgorithm::from_name and therefore
2002                    // we can do a simple equals check here for both algorithm and value.
2003                    if !integrity_expressions.iter().any(|ex| ex == source) {
2004                        return DoesNotMatch;
2005                    }
2006                }
2007                // Step 7: Return "Matches".
2008                Matches
2009            }
2010        }
2011    }
2012    /// https://www.w3.org/TR/CSP/#match-request-to-source-list
2013    fn does_request_match_source_list(&self, request: &Request) -> MatchResult {
2014        // > Given a request request, a source list source list, and an origin self-origin,
2015        // > this algorithm returns the result of executing
2016        // > § 6.7.2.7 Does url match source list in origin with redirect count?
2017        // > on request’s current url, source list, self-origin, and request’s redirect count.
2018        self.does_url_match_source_list_in_origin_with_redirect_count(
2019            &request.current_url,
2020            &request.origin,
2021            request.redirect_count,
2022        )
2023    }
2024    /// https://www.w3.org/TR/CSP/#match-url-to-source-list
2025    fn does_url_match_source_list_in_origin_with_redirect_count(
2026        &self,
2027        url: &Url,
2028        origin: &Origin,
2029        redirect_count: u32,
2030    ) -> MatchResult {
2031        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2032            if NONE_SOURCE_GRAMMAR.is_match(expression) {
2033                continue;
2034            };
2035            let result = does_url_match_expression_in_origin_with_redirect_count(
2036                url,
2037                expression,
2038                origin,
2039                redirect_count,
2040            );
2041            if result == Matches {
2042                return Matches;
2043            }
2044        }
2045        DoesNotMatch
2046    }
2047    /// https://www.w3.org/TR/CSP/#match-element-to-source-list
2048    fn does_element_match_source_list_for_type_and_source(
2049        &self,
2050        element: &Element,
2051        type_: InlineCheckType,
2052        source: &str,
2053    ) -> MatchResult {
2054        if self.does_a_source_list_allow_all_inline_behavior_for_type(type_) == AllowResult::Allows
2055        {
2056            return Matches;
2057        }
2058        if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style {
2059            if let Some(nonce) = element.nonce.as_ref() {
2060                for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2061                    if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression) {
2062                        if let Some(captured_nonce) = captures.name("n") {
2063                            if nonce == captured_nonce.as_str() {
2064                                return Matches;
2065                            }
2066                        }
2067                    }
2068                }
2069            }
2070        }
2071        let mut unsafe_hashes = false;
2072        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2073            if ascii_case_insensitive_match(expression, "'unsafe-hashes'") {
2074                unsafe_hashes = true;
2075                break;
2076            }
2077        }
2078        if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style || unsafe_hashes {
2079            for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2080                if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression) {
2081                    if let (Some(algorithm), Some(value)) = (
2082                        captures
2083                            .name("algorithm")
2084                            .and_then(|a| HashAlgorithm::from_name(a.as_str())),
2085                        captures.name("value"),
2086                    ) {
2087                        let actual = algorithm.apply(source);
2088                        let expected = value.as_str().replace('-', "+").replace('_', "/");
2089                        if actual == expected {
2090                            return Matches;
2091                        }
2092                    }
2093                }
2094            }
2095        }
2096        DoesNotMatch
2097    }
2098    /// https://www.w3.org/TR/CSP/#allow-all-inline
2099    fn does_a_source_list_allow_all_inline_behavior_for_type(
2100        &self,
2101        type_: InlineCheckType,
2102    ) -> AllowResult {
2103        use InlineCheckType::*;
2104        let mut allow_all_inline = false;
2105        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2106            if HASH_SOURCE_GRAMMAR.is_match(expression) || NONCE_SOURCE_GRAMMAR.is_match(expression)
2107            {
2108                return AllowResult::DoesNotAllow;
2109            }
2110            if (type_ == Script || type_ == ScriptAttribute || type_ == Navigation)
2111                && expression == "'strict-dynamic'"
2112            {
2113                return AllowResult::DoesNotAllow;
2114            }
2115            if ascii_case_insensitive_match(expression, "'unsafe-inline'") {
2116                allow_all_inline = true;
2117            }
2118        }
2119        if allow_all_inline {
2120            AllowResult::Allows
2121        } else {
2122            AllowResult::DoesNotAllow
2123        }
2124    }
2125    /// https://www.w3.org/TR/CSP/#match-response-to-source-list
2126    fn does_response_to_request_match_source_list(
2127        &self,
2128        request: &Request,
2129        response: &Response,
2130    ) -> MatchResult {
2131        self.does_url_match_source_list_in_origin_with_redirect_count(
2132            &response.url,
2133            &request.origin,
2134            response.redirect_count,
2135        )
2136    }
2137    /// https://www.w3.org/TR/CSP/#can-compile-strings
2138    fn does_a_source_list_allow_js_evaluation(&self) -> AllowResult {
2139        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2140            // Step 5.3: If source-list contains a source expression which is an ASCII case-insensitive match
2141            // for the string "'unsafe-eval'", then skip the following steps.
2142            if ascii_case_insensitive_match(expression, "'unsafe-eval'") {
2143                return AllowResult::Allows;
2144            }
2145        }
2146        AllowResult::DoesNotAllow
2147    }
2148    /// https://www.w3.org/TR/CSP/#can-compile-wasm-bytes
2149    fn does_a_source_list_allow_wasm_evaluation(&self) -> AllowResult {
2150        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2151            if ascii_case_insensitive_match(expression, "'unsafe-eval'")
2152                || ascii_case_insensitive_match(expression, "'wasm-unsafe-eval'")
2153            {
2154                return AllowResult::Allows;
2155            }
2156        }
2157        AllowResult::DoesNotAllow
2158    }
2159}
2160
2161/// https://www.w3.org/TR/CSP/#allow-all-inline
2162#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2163enum AllowResult {
2164    Allows,
2165    DoesNotAllow,
2166}
2167
2168/// https://www.w3.org/TR/CSP/#match-url-to-source-expression
2169fn does_url_match_expression_in_origin_with_redirect_count(
2170    url: &Url,
2171    expression: &str,
2172    origin: &Origin,
2173    redirect_count: u32,
2174) -> MatchResult {
2175    let url_scheme = url.scheme();
2176    if expression == "*" {
2177        if scheme_is_network(url_scheme) {
2178            return Matches;
2179        }
2180        return origin_scheme_part_match(origin, url_scheme);
2181    }
2182    if let Some(captures) = SCHEME_SOURCE_GRAMMAR.captures(expression) {
2183        if let Some(expression_scheme) = captures.name("scheme") {
2184            return scheme_part_match(expression_scheme.as_str(), url_scheme);
2185        }
2186        // It should not be possible to match HOST_SOURCE_GRAMMAR without having a scheme part
2187        return DoesNotMatch;
2188    }
2189    if let Some(captures) = HOST_SOURCE_GRAMMAR.captures(expression) {
2190        let expr_has_scheme_part = if let Some(expression_scheme) = captures.name("scheme") {
2191            if scheme_part_match(expression_scheme.as_str(), url_scheme) != Matches {
2192                return DoesNotMatch;
2193            }
2194            true
2195        } else {
2196            false
2197        };
2198        let url_host = if let Some(url_host) = url.host() {
2199            url_host
2200        } else {
2201            return DoesNotMatch;
2202        };
2203        if !expr_has_scheme_part && origin_scheme_part_match(origin, url.scheme()) != Matches {
2204            return DoesNotMatch;
2205        }
2206        if let Some(expression_host) = captures.name("host") {
2207            if host_part_match(expression_host.as_str(), &url_host.to_string()) != Matches {
2208                return DoesNotMatch;
2209            }
2210        } else {
2211            // It should not be possible to match HOST_SOURCE_GRAMMAR without having a host part
2212            return DoesNotMatch;
2213        }
2214        // Skip the first byte of the port capture to avoid the `:`.
2215        let port_part = captures.name("port").map(|port| &port.as_str()[1..]);
2216        if port_part_match(port_part, url) != Matches {
2217            return DoesNotMatch;
2218        }
2219        let path_part = captures
2220            .name("path")
2221            .map(|path_part| path_part.as_str())
2222            .unwrap_or("");
2223        if path_part != "/" && redirect_count == 0 {
2224            let path = url.path();
2225            if path_part_match(path_part, path) != Matches {
2226                return DoesNotMatch;
2227            }
2228        }
2229        return Matches;
2230    }
2231    if ascii_case_insensitive_match(expression, "'self'") {
2232        if *origin == url.origin() {
2233            return Matches;
2234        }
2235        if let Origin::Tuple(scheme, host, port) = origin {
2236            let hosts_are_the_same = Some(host) == url.host().map(|p| p.to_owned()).as_ref();
2237            let ports_are_the_same = Some(*port) == url.port();
2238            let origins_port_is_default_for_scheme = Some(*port) == default_port(scheme);
2239            let url_port_is_default_port_for_scheme =
2240                url.port() == default_port(scheme) && default_port(scheme).is_some();
2241            let ports_are_default =
2242                url_port_is_default_port_for_scheme && origins_port_is_default_for_scheme;
2243            if hosts_are_the_same
2244                && (ports_are_the_same || ports_are_default)
2245                && ((url_scheme == "https" || url_scheme == "wss")
2246                    || (scheme == "http" && (url_scheme == "http" || url_scheme == "ws")))
2247            {
2248                return Matches;
2249            }
2250        }
2251    }
2252    DoesNotMatch
2253}
2254
2255/// https://www.w3.org/TR/CSP/#match-hosts
2256fn host_part_match(pattern: &str, host: &str) -> MatchResult {
2257    debug_assert!(!host.is_empty());
2258    // Step 1. If host is not a domain, return "Does Not Match".
2259    if host.is_empty() {
2260        return DoesNotMatch;
2261    }
2262    if pattern.as_bytes()[0] == b'*' {
2263        // Step 2. If pattern is "*", return "Matches".
2264        if pattern.len() == 1 {
2265            return Matches;
2266        }
2267        // Step 3. If pattern starts with "*.":
2268        if pattern.as_bytes()[1] == b'.' {
2269            // Step 3.1 Let remaining be pattern with the leading U+002A (*) removed and ASCII lowercased.
2270            let remaining_pattern = &pattern[1..];
2271            if remaining_pattern.len() > host.len() {
2272                return DoesNotMatch;
2273            }
2274            let remaining_host = &host[(host.len() - remaining_pattern.len())..];
2275            debug_assert_eq!(remaining_host.len(), remaining_pattern.len());
2276            // Step 3.2. If host to ASCII lowercase ends with remaining, then return "Matches".
2277            if ascii_case_insensitive_match(remaining_pattern, remaining_host) {
2278                return Matches;
2279            }
2280            // Step 3.3 Return "Does Not Match".
2281            return DoesNotMatch;
2282        }
2283    }
2284    // Step 4. If pattern is not an ASCII case-insensitive match for host, return "Does Not Match".
2285    if !ascii_case_insensitive_match(pattern, host) {
2286        return DoesNotMatch;
2287    }
2288    static IPV4_ADDRESS_RULE: Lazy<Regex> = Lazy::new(|| {
2289        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()
2290    });
2291    if IPV4_ADDRESS_RULE.is_match(pattern) && pattern != "127.0.0.1" {
2292        return DoesNotMatch;
2293    }
2294    // The spec uses the phrase "if A is an IPv6 address", without giving specific instructions on
2295    // how to tell if this is the case. In URLs, IPv6 addresses start with `[`, so let's go with that.
2296    // See https://url.spec.whatwg.org/#host-parsing
2297    if pattern.as_bytes()[0] == b'[' {
2298        return DoesNotMatch;
2299    }
2300    // Step 5. Return "Matches".
2301    Matches
2302}
2303
2304/// https://www.w3.org/TR/CSP/#match-ports
2305fn port_part_match(input: Option<&str>, url: &Url) -> MatchResult {
2306    use std::str::FromStr;
2307    // 1. Assert: input is null, "*", or a sequence of one or more ASCII digits.
2308    debug_assert!(input.is_none() || input == Some("*") || u16::from_str(input.unwrap()).is_ok());
2309    // 2. If input is equal to "*", return "Matches".
2310    if input == Some("*") {
2311        return Matches;
2312    }
2313    // 3. Let normalizedInput be null if input null; otherwise input interpreted as decimal number.
2314    let normalized_input = if let Some(input) = input {
2315        u16::from_str(&input).ok()
2316    } else {
2317        None
2318    };
2319    // 4. If normalizedInput equals url’s port, return "Matches".
2320    if normalized_input == url.port() {
2321        return Matches;
2322    }
2323    // 5. If url’s port is null:
2324    if url.port().is_none() {
2325        // 5.1. Let defaultPort be the default port for url’s scheme.
2326        let default_port = default_port(url.scheme());
2327        // 5.2. If normalizedInput equals defaultPort, return "Matches".
2328        if normalized_input == default_port {
2329            return Matches;
2330        }
2331    }
2332    // 6. Return "Does Not Match".
2333    DoesNotMatch
2334}
2335
2336/// https://www.w3.org/TR/CSP/#match-paths
2337fn path_part_match(path_a: &str, path_b: &str) -> MatchResult {
2338    if path_a.is_empty() {
2339        return Matches;
2340    }
2341    if path_a == "/" && path_b.is_empty() {
2342        return Matches;
2343    }
2344    let exact_match = path_a.as_bytes()[path_a.len() - 1] != b'/';
2345    let (mut path_list_a, path_list_b): (Vec<&str>, Vec<&str>) =
2346        (path_a.split('/').collect(), path_b.split('/').collect());
2347    if path_list_a.len() > path_list_b.len() {
2348        return DoesNotMatch;
2349    }
2350    if exact_match && path_list_a.len() != path_list_b.len() {
2351        return DoesNotMatch;
2352    }
2353    if !exact_match {
2354        debug_assert_eq!(path_list_a[path_list_a.len() - 1], "");
2355        path_list_a.pop();
2356    }
2357    let mut piece_b_iter = path_list_b.iter();
2358    for piece_a in &path_list_a {
2359        let piece_b = piece_b_iter.next().unwrap();
2360        let piece_a: Vec<u8> = percent_encoding::percent_decode(piece_a.as_bytes()).collect();
2361        let piece_b: Vec<u8> = percent_encoding::percent_decode(piece_b.as_bytes()).collect();
2362        if piece_a != piece_b {
2363            return DoesNotMatch;
2364        }
2365    }
2366    Matches
2367}
2368
2369fn default_port(scheme: &str) -> Option<u16> {
2370    Some(match scheme {
2371        "ftp" => 21,
2372        "gopher" => 70,
2373        "http" => 80,
2374        "https" => 443,
2375        "ws" => 80,
2376        "wss" => 443,
2377        _ => return None,
2378    })
2379}
2380
2381fn origin_scheme_part_match(a: &Origin, b: &str) -> MatchResult {
2382    if let Origin::Tuple(scheme, _host, _port) = a {
2383        scheme_part_match(&scheme[..], b)
2384    } else {
2385        DoesNotMatch
2386    }
2387}
2388
2389/// https://www.w3.org/TR/CSP/#match-schemes
2390fn scheme_part_match(a: &str, b: &str) -> MatchResult {
2391    let a = a.to_ascii_lowercase();
2392    let b = b.to_ascii_lowercase();
2393    match (&a[..], &b[..]) {
2394        _ if a == b => Matches,
2395        ("http", "https") | ("ws", "wss" | "http" | "https") | ("wss", "https") => Matches,
2396        _ => DoesNotMatch,
2397    }
2398}
2399
2400#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2401pub enum HashAlgorithm {
2402    Sha256,
2403    Sha384,
2404    Sha512,
2405}
2406
2407impl HashAlgorithm {
2408    pub fn from_name(name: &str) -> Option<HashAlgorithm> {
2409        use HashAlgorithm::*;
2410        match name {
2411            "sha256" | "Sha256" | "sHa256" | "shA256" | "SHa256" | "ShA256" | "sHA256"
2412            | "SHA256" => Some(Sha256),
2413            "sha384" | "Sha384" | "sHa384" | "shA384" | "SHa384" | "ShA384" | "sHA384"
2414            | "SHA384" => Some(Sha384),
2415            "sha512" | "Sha512" | "sHa512" | "shA512" | "SHa512" | "ShA512" | "sHA512"
2416            | "SHA512" => Some(Sha512),
2417            _ => None,
2418        }
2419    }
2420    pub fn apply(self, value: &str) -> String {
2421        use base64::Engine as _;
2422        let bytes = value.as_bytes();
2423        let standard = base64::engine::general_purpose::STANDARD;
2424        match self {
2425            HashAlgorithm::Sha256 => standard.encode(sha2::Sha256::digest(bytes)),
2426            HashAlgorithm::Sha384 => standard.encode(sha2::Sha384::digest(bytes)),
2427            HashAlgorithm::Sha512 => standard.encode(sha2::Sha512::digest(bytes)),
2428        }
2429    }
2430}
2431
2432/// https://www.w3.org/TR/SRI/#integrity-metadata
2433#[derive(Clone, Debug, Eq, PartialEq)]
2434pub struct HashFunction {
2435    algorithm: HashAlgorithm,
2436    value: String,
2437    // The spec defines a third member, options, but defines no values.
2438}
2439
2440/// https://www.w3.org/TR/SRI/#parse-metadata
2441#[derive(Clone, Debug, Eq, PartialEq)]
2442pub enum SubresourceIntegrityMetadata {
2443    NoMetadata,
2444    IntegritySources(Vec<HashFunction>),
2445}
2446
2447/// https://www.w3.org/TR/SRI/#the-integrity-attribute
2448/// This corresponds to the "hash-expression" grammar.
2449static SUBRESOURCE_METADATA_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
2450    Regex::new(r#"(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)"#)
2451        .unwrap()
2452});
2453
2454/// https://www.w3.org/TR/SRI/#parse-metadata
2455pub fn parse_subresource_integrity_metadata(string: &str) -> SubresourceIntegrityMetadata {
2456    let mut result = Vec::new();
2457    let mut empty = true;
2458    for token in split_ascii_whitespace(string) {
2459        empty = false;
2460        if let Some(captures) = SUBRESOURCE_METADATA_GRAMMAR.captures(token) {
2461            if let (Some(algorithm), Some(value)) = (
2462                captures
2463                    .name("algorithm")
2464                    .and_then(|a| HashAlgorithm::from_name(a.as_str())),
2465                captures.name("value"),
2466            ) {
2467                result.push(HashFunction {
2468                    algorithm,
2469                    value: String::from(value.as_str()),
2470                });
2471            }
2472        }
2473    }
2474    if empty {
2475        SubresourceIntegrityMetadata::NoMetadata
2476    } else {
2477        SubresourceIntegrityMetadata::IntegritySources(result)
2478    }
2479}
2480
2481#[cfg(test)]
2482mod test {
2483    use super::*;
2484    #[test]
2485    fn empty_directive_is_not_valid() {
2486        let d = Directive {
2487            name: String::new(),
2488            value: Vec::new(),
2489        };
2490        assert!(!d.is_valid());
2491    }
2492    #[test]
2493    pub fn duplicate_policy_is_not_valid() {
2494        let d = Directive {
2495            name: "test".to_owned(),
2496            value: vec!["test".to_owned()],
2497        };
2498        let p = Policy {
2499            directive_set: vec![d.clone(), d.clone()],
2500            disposition: PolicyDisposition::Enforce,
2501            source: PolicySource::Header,
2502        };
2503        assert!(!p.is_valid());
2504    }
2505    #[test]
2506    pub fn basic_policy_is_valid() {
2507        let p = Policy::parse(
2508            "script-src notriddle.com",
2509            PolicySource::Header,
2510            PolicyDisposition::Enforce,
2511        );
2512        assert!(p.is_valid());
2513    }
2514    #[test]
2515    pub fn policy_with_empty_directive_set_is_not_valid() {
2516        let p = Policy {
2517            directive_set: vec![],
2518            disposition: PolicyDisposition::Enforce,
2519            source: PolicySource::Header,
2520        };
2521        assert!(!p.is_valid());
2522    }
2523
2524    #[test]
2525    pub fn prefetch_request_does_not_violate_policy() {
2526        let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2527        let request = Request {
2528            url: url.clone(),
2529            current_url: url,
2530            origin: Origin::Tuple(
2531                "https".to_string(),
2532                url::Host::Domain("notriddle.com".to_owned()),
2533                443,
2534            ),
2535            redirect_count: 0,
2536            destination: Destination::Script,
2537            initiator: Initiator::Prefetch,
2538            nonce: String::new(),
2539            integrity_metadata: String::new(),
2540            parser_metadata: ParserMetadata::None,
2541        };
2542
2543        let p = Policy::parse(
2544            "child-src 'self'",
2545            PolicySource::Header,
2546            PolicyDisposition::Enforce,
2547        );
2548
2549        let violation_result = p.does_request_violate_policy(&request);
2550
2551        assert!(violation_result == Violates::DoesNotViolate);
2552    }
2553
2554    #[test]
2555    pub fn prefetch_request_violates_policy() {
2556        let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2557        let request = Request {
2558            url: url.clone(),
2559            current_url: url,
2560            origin: Origin::Tuple(
2561                "https".to_string(),
2562                url::Host::Domain("notriddle.com".to_owned()),
2563                443,
2564            ),
2565            redirect_count: 0,
2566            destination: Destination::ServiceWorker,
2567            initiator: Initiator::None,
2568            nonce: String::new(),
2569            integrity_metadata: String::new(),
2570            parser_metadata: ParserMetadata::None,
2571        };
2572
2573        let p = Policy::parse(
2574            "default-src 'none'; script-src 'self' ",
2575            PolicySource::Header,
2576            PolicyDisposition::Enforce,
2577        );
2578
2579        let violation_result = p.does_request_violate_policy(&request);
2580
2581        let expected_result = Violates::Directive(Directive {
2582            name: String::from("script-src"),
2583            value: vec![String::from("'self'")],
2584        });
2585
2586        assert!(violation_result == expected_result);
2587    }
2588
2589    #[test]
2590    pub fn prefetch_request_is_allowed_by_directive() {
2591        let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2592        let request = Request {
2593            url: url.clone(),
2594            current_url: url,
2595            origin: Origin::Tuple(
2596                "https".to_string(),
2597                url::Host::Domain("notriddle.com".to_owned()),
2598                443,
2599            ),
2600            redirect_count: 0,
2601            destination: Destination::Script,
2602            initiator: Initiator::Prefetch,
2603            nonce: String::new(),
2604            integrity_metadata: String::new(),
2605            parser_metadata: ParserMetadata::None,
2606        };
2607
2608        let p = Policy::parse(
2609            "default-src 'none'; child-src 'self'",
2610            PolicySource::Header,
2611            PolicyDisposition::Enforce,
2612        );
2613
2614        let violation_result = p.does_request_violate_policy(&request);
2615
2616        assert!(violation_result == Violates::DoesNotViolate);
2617    }
2618
2619    #[test]
2620    pub fn websocket_request_is_allowed_by_directive() {
2621        let url = Url::parse("https://www.notriddle.com/websocket").unwrap();
2622        let request = Request {
2623            url: url.clone(),
2624            current_url: url,
2625            origin: Origin::Tuple(
2626                "https".to_string(),
2627                url::Host::Domain("notriddle.com".to_owned()),
2628                443,
2629            ),
2630            redirect_count: 0,
2631            destination: Destination::None,
2632            initiator: Initiator::None,
2633            nonce: String::new(),
2634            integrity_metadata: String::new(),
2635            parser_metadata: ParserMetadata::None,
2636        };
2637
2638        let p = Policy::parse(
2639            "connect-src ws://www.notriddle.com/websocket",
2640            PolicySource::Header,
2641            PolicyDisposition::Enforce,
2642        );
2643
2644        let violation_result = p.does_request_violate_policy(&request);
2645
2646        assert!(violation_result == Violates::DoesNotViolate);
2647    }
2648
2649    #[test]
2650    pub fn trusted_type_policy_is_valid() {
2651        let p = Policy::parse(
2652            "trusted-types 'none'",
2653            PolicySource::Meta,
2654            PolicyDisposition::Enforce,
2655        );
2656        assert!(p.is_valid());
2657        assert_eq!(p.directive_set[0].value, vec!["'none'".to_owned()]);
2658    }
2659
2660    #[test]
2661    pub fn csp_list_is_valid() {
2662        let csp_list = CspList::parse(
2663            "default-src 'none'; child-src 'self', trusted-types 'none'",
2664            PolicySource::Meta,
2665            PolicyDisposition::Enforce,
2666        );
2667        assert!(csp_list.is_valid());
2668        assert_eq!(
2669            csp_list.0[1].directive_set[0].value,
2670            vec!["'none'".to_owned()]
2671        );
2672    }
2673
2674    #[test]
2675    pub fn no_trusted_types_specified_allows_all_policies() {
2676        let csp_list = CspList::parse(
2677            "default-src 'none'; child-src 'self'",
2678            PolicySource::Meta,
2679            PolicyDisposition::Enforce,
2680        );
2681        assert!(csp_list.is_valid());
2682        let (check_result, violations) =
2683            csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2684        assert_eq!(check_result, CheckResult::Allowed);
2685        assert!(violations.is_empty());
2686    }
2687
2688    #[test]
2689    pub fn none_does_not_allow_for_any_policy() {
2690        let csp_list = CspList::parse(
2691            "trusted-types 'none'",
2692            PolicySource::Meta,
2693            PolicyDisposition::Enforce,
2694        );
2695        assert!(csp_list.is_valid());
2696        let (check_result, violations) =
2697            csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2698        assert!(check_result == CheckResult::Blocked);
2699        assert_eq!(violations.len(), 1);
2700    }
2701
2702    #[test]
2703    pub fn extra_none_allows_all_policies() {
2704        let csp_list = CspList::parse(
2705            "trusted-types some-policy 'none'",
2706            PolicySource::Meta,
2707            PolicyDisposition::Enforce,
2708        );
2709        assert!(csp_list.is_valid());
2710        let (check_result, violations) =
2711            csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2712        assert!(check_result == CheckResult::Allowed);
2713        assert!(violations.is_empty());
2714    }
2715
2716    #[test]
2717    pub fn explicit_policy_named_is_allowed() {
2718        let csp_list = CspList::parse(
2719            "trusted-types MyPolicy",
2720            PolicySource::Meta,
2721            PolicyDisposition::Enforce,
2722        );
2723        assert!(csp_list.is_valid());
2724        let (check_result, violations) =
2725            csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2726        assert_eq!(check_result, CheckResult::Allowed);
2727        assert!(violations.is_empty());
2728    }
2729
2730    #[test]
2731    pub fn other_policy_name_is_blocked() {
2732        let csp_list = CspList::parse(
2733            "trusted-types MyPolicy",
2734            PolicySource::Meta,
2735            PolicyDisposition::Enforce,
2736        );
2737        assert!(csp_list.is_valid());
2738        let (check_result, violations) =
2739            csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2740        assert!(check_result == CheckResult::Blocked);
2741        assert_eq!(violations.len(), 1);
2742    }
2743
2744    #[test]
2745    pub fn invalid_characters_in_policy_name_is_blocked() {
2746        let csp_list = CspList::parse(
2747            "trusted-types My?Policy",
2748            PolicySource::Meta,
2749            PolicyDisposition::Enforce,
2750        );
2751        assert!(csp_list.is_valid());
2752        let (check_result, violations) =
2753            csp_list.is_trusted_type_policy_creation_allowed("My?Policy", &["My?Policy"]);
2754        assert!(check_result == CheckResult::Blocked);
2755        assert_eq!(violations.len(), 1);
2756    }
2757
2758    #[test]
2759    pub fn already_created_policy_is_blocked() {
2760        let csp_list = CspList::parse(
2761            "trusted-types MyPolicy",
2762            PolicySource::Meta,
2763            PolicyDisposition::Enforce,
2764        );
2765        assert!(csp_list.is_valid());
2766        let (check_result, violations) =
2767            csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2768        assert!(check_result == CheckResult::Blocked);
2769        assert_eq!(violations.len(), 1);
2770    }
2771
2772    #[test]
2773    pub fn already_created_policy_is_allowed_with_allow_duplicates() {
2774        let csp_list = CspList::parse(
2775            "trusted-types MyPolicy 'allow-duplicates'",
2776            PolicySource::Meta,
2777            PolicyDisposition::Enforce,
2778        );
2779        assert!(csp_list.is_valid());
2780        let (check_result, violations) =
2781            csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2782        assert!(check_result == CheckResult::Allowed);
2783        assert!(violations.is_empty());
2784    }
2785
2786    #[test]
2787    pub fn only_report_policy_issues_for_disposition_report() {
2788        let csp_list = CspList::parse(
2789            "trusted-types MyPolicy",
2790            PolicySource::Meta,
2791            PolicyDisposition::Report,
2792        );
2793        assert!(csp_list.is_valid());
2794        let (check_result, violations) =
2795            csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2796        assert!(check_result == CheckResult::Allowed);
2797        assert_eq!(violations.len(), 1);
2798    }
2799
2800    #[test]
2801    pub fn wildcard_allows_all_policies() {
2802        let csp_list = CspList::parse(
2803            "trusted-types *",
2804            PolicySource::Meta,
2805            PolicyDisposition::Report,
2806        );
2807        assert!(csp_list.is_valid());
2808        let (check_result, violations) =
2809            csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2810        assert!(check_result == CheckResult::Allowed);
2811        assert!(violations.is_empty());
2812    }
2813
2814    #[test]
2815    pub fn violation_has_correct_directive() {
2816        let csp_list = CspList::parse(
2817            "trusted-types MyPolicy",
2818            PolicySource::Meta,
2819            PolicyDisposition::Enforce,
2820        );
2821        assert!(csp_list.is_valid());
2822        let (check_result, violations) =
2823            csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2824        assert!(check_result == CheckResult::Blocked);
2825        assert_eq!(violations.len(), 1);
2826        assert_eq!(violations[0].directive, csp_list.0[0].directive_set[0]);
2827    }
2828
2829    #[test]
2830    pub fn long_policy_name_is_truncated() {
2831        let csp_list = CspList::parse(
2832            "trusted-types MyPolicy",
2833            PolicySource::Meta,
2834            PolicyDisposition::Enforce,
2835        );
2836        assert!(csp_list.is_valid());
2837        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed(
2838            "SuperLongPolicyNameThatExceeds40Characters",
2839            &[],
2840        );
2841        assert!(check_result == CheckResult::Blocked);
2842        assert_eq!(violations.len(), 1);
2843        assert!(
2844            matches!(&violations[0].resource, ViolationResource::TrustedTypePolicy { sample } if sample == "SuperLongPolicyNameThatExceeds40Characte")
2845        );
2846    }
2847}