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