script/dom/security/xframeoptions.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use content_security_policy::{CspList, PolicyDisposition};
6use http::header::HeaderMap;
7use hyper_serde::Serde;
8use net_traits::fetch::headers::get_decode_and_split_header_name;
9use rustc_hash::FxHashSet;
10use servo_url::MutableOrigin;
11
12use crate::dom::node::NodeTraits;
13use crate::dom::window::Window;
14
15/// <https://html.spec.whatwg.org/multipage/#check-a-navigation-response%027s-adherence-to-x-frame-options>
16pub(crate) fn check_a_navigation_response_adherence_to_x_frame_options(
17 window: &Window,
18 csp_list: Option<&CspList>,
19 destination_origin: &MutableOrigin,
20 headers: Option<&Serde<HeaderMap>>,
21) -> bool {
22 // Step 1. If navigable is not a child navigable, then return true.
23 if window.window_proxy().parent().is_none() {
24 return true;
25 }
26 // Step 2. For each policy of cspList:
27 if let Some(csp_list) = csp_list {
28 for policy in csp_list.0.iter() {
29 // Step 2.1. If policy's disposition is not "enforce", then continue.
30 if policy.disposition != PolicyDisposition::Enforce {
31 continue;
32 }
33 // Step 2.2. If policy's directive set contains a frame-ancestors directive, then return true.
34 if policy.contains_a_directive_whose_name_is("frame-ancestors") {
35 return true;
36 }
37 }
38 }
39 // Step 3. Let rawXFrameOptions be the result of getting, decoding,
40 // and splitting `X-Frame-Options` from response's header list.
41 let Some(headers) = headers else {
42 return true;
43 };
44 let Some(raw_xframe_options) = get_decode_and_split_header_name("X-Frame-Options", headers)
45 else {
46 return true;
47 };
48 // Step 4. Let xFrameOptions be a new set.
49 // Step 5. For each value of rawXFrameOptions, append value, converted to ASCII lowercase, to xFrameOptions.
50 let x_frame_options =
51 FxHashSet::from_iter(raw_xframe_options.iter().map(|value| value.to_lowercase()));
52 // Step 6. If xFrameOptions's size is greater than 1,
53 // and xFrameOptions contains any of "deny", "allowall", or "sameorigin", then return false.
54 if x_frame_options.len() > 1 &&
55 x_frame_options
56 .iter()
57 .any(|value| value == "deny" || value == "allowall" || value == "sameorigin")
58 {
59 return false;
60 }
61 // Step 7. If xFrameOptions's size is greater than 1, then return true.
62 if x_frame_options.len() > 1 {
63 return true;
64 }
65 let Some(first_item) = x_frame_options.iter().next() else {
66 return true;
67 };
68 // Step 8. If xFrameOptions[0] is "deny", then return false.
69 if first_item == "deny" {
70 return false;
71 }
72 // Step 9. If xFrameOptions[0] is "sameorigin", then:
73 if first_item == "sameorigin" {
74 let mut window_proxy = window.window_proxy();
75 // Step 9.2. While containerDocument is not null:
76 while let Some(container_element) = window_proxy.frame_element() {
77 // Step 9.1. Let containerDocument be navigable's container document.
78 let container_document = container_element.owner_document();
79 // Step 9.2.1. If containerDocument's origin is not same origin with destinationOrigin, then return false.
80 if !container_document.origin().same_origin(destination_origin) {
81 return false;
82 }
83 // Step 9.2.2. Set containerDocument to containerDocument's container document.
84 window_proxy = container_document.window().window_proxy()
85 }
86 // If the `frame_element` is None, it could be two options:
87 // 1. There is no parent, in which case we are top-level. We can stop the loop
88 // 2. There is a parent, but it isn't same origin. Therefore, we need to double
89 // check here that we actually cover this case.
90 if window_proxy.parent().is_some() {
91 return false;
92 }
93 }
94 // Step 10. Return true.
95 true
96}