diff --git a/components/net/fetch/headers.rs b/components/net/fetch/headers.rs new file mode 100644 index 00000000000..83e515a2305 --- /dev/null +++ b/components/net/fetch/headers.rs @@ -0,0 +1,173 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use headers::HeaderMap; +use std::iter::Peekable; +use std::str::Chars; + +/// +const HTTP_TAB_OR_SPACE: &[char] = &['\u{0009}', '\u{0020}']; + +/// +pub fn determine_nosniff(headers: &HeaderMap) -> bool { + let values = get_header_value_as_list("x-content-type-options", headers); + + match values { + None => false, + Some(values) => !values.is_empty() && (&values[0]).eq_ignore_ascii_case("nosniff"), + } +} + +/// +fn get_header_value_as_list(name: &str, headers: &HeaderMap) -> Option> { + fn char_is_not_quote_or_comma(c: char) -> bool { + return c != '\u{0022}' && c != '\u{002C}'; + } + + // Step 1 + let initial_value = get_value_from_header_list(name, headers); + + if let Some(input) = initial_value { + // Step 4 + let mut position = input.chars().peekable(); + + // Step 5 + let mut values: Vec = vec![]; + + // Step 6 + let mut value = String::new(); + + // Step 7 + while position.peek().is_some() { + // Step 7.1 + value += &*collect_sequence(&mut position, char_is_not_quote_or_comma); + + // Step 7.2 + if let Some(&ch) = position.peek() { + if ch == '\u{0022}' { + // Step 7.2.1.1 + value += &*collect_http_quoted_string(&mut position, false); + + // Step 7.2.1.2 + if position.peek().is_some() { + continue; + } + } else { + // ch == '\u{002C}' + + // Step 7.2.2.2 + position.next(); + } + } + + // Step 7.3 + value = value.trim_matches(HTTP_TAB_OR_SPACE).to_string(); + + // Step 7.4 + values.push(value); + + // Step 7.5 + value = String::new(); + } + + return Some(values); + } + + // Step 2 + return None; +} + +/// +fn collect_sequence(position: &mut Peekable, condition: F) -> String +where + F: Fn(char) -> bool, +{ + // Step 1 + let mut result = String::new(); + + // Step 2 + while let Some(&ch) = position.peek() { + if !condition(ch) { + break; + } + result.push(ch); + position.next(); + } + + // Step 3 + return result; +} + +/// +fn collect_http_quoted_string(position: &mut Peekable, extract_value: bool) -> String { + fn char_is_not_quote_or_backslash(c: char) -> bool { + return c != '\u{0022}' && c != '\u{005C}'; + } + + // Step 2 + // We will store the 'extracted value' or the raw value + let mut value = String::new(); + + // Step 3, 4 + let should_be_quote = position.next(); + if let Some(ch) = should_be_quote { + if !extract_value { + value.push(ch) + } + } + + // Step 5 + loop { + // Step 5.1 + value += &*collect_sequence(position, char_is_not_quote_or_backslash); + + // Step 5.2 + if position.peek().is_none() { + break; + } + + // Step 5.3, 5.4 + let quote_or_backslash = position.next().unwrap(); + if !extract_value { + value.push(quote_or_backslash); + } + + if quote_or_backslash == '\u{005C}' { + if let Some(ch) = position.next() { + value.push(ch); + } else { + // Step 5.5.1 + if extract_value { + value.push('\u{005C}'); + } + break; + } + } else { + // Step 5.6.1 + // assert quote_or_backslash is a quote + + // Step 5.6.2 + break; + } + } + + // Step 6, 7 + return value; +} + +/// +fn get_value_from_header_list(name: &str, headers: &HeaderMap) -> Option { + let values = headers + .get_all(name) + .iter() + .map(|val| val.to_str().unwrap()); + + // Step 1 + if values.size_hint() == (0, Some(0)) { + return None; + } + + // Step 2 + return Some(values.collect::>().join(", ")); +} diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs index fd76d71ad83..1895642e2ce 100644 --- a/components/net/fetch/methods.rs +++ b/components/net/fetch/methods.rs @@ -4,6 +4,7 @@ use crate::data_loader::decode; use crate::fetch::cors_cache::CorsCache; +use crate::fetch::headers::determine_nosniff; use crate::filemanager_thread::{FileManager, FILE_CHUNK_SIZE}; use crate::http_loader::{determine_requests_referrer, http_fetch, HttpState}; use crate::http_loader::{set_default_accept, set_default_accept_language}; @@ -834,18 +835,12 @@ pub fn should_be_blocked_due_to_nosniff( destination: Destination, response_headers: &HeaderMap, ) -> bool { - // Steps 1-3. - // TODO(eijebong): Replace this once typed headers allow custom ones... - if response_headers - .get("x-content-type-options") - .map_or(true, |val| { - val.to_str().unwrap_or("").to_lowercase() != "nosniff" - }) - { + // Step 1 + if !determine_nosniff(response_headers) { return false; } - // Step 4 + // Step 2 // Note: an invalid MIME type will produce a `None`. let content_type_header = response_headers.typed_get::(); @@ -877,19 +872,19 @@ pub fn should_be_blocked_due_to_nosniff( } match content_type_header { - // Step 6 + // Step 4 Some(ref ct) if destination.is_script_like() => { !is_javascript_mime_type(&ct.clone().into()) }, - // Step 7 + // Step 5 Some(ref ct) if destination == Destination::Style => { let m: mime::Mime = ct.clone().into(); m.type_() != mime::TEXT && m.subtype() != mime::CSS }, None if destination == Destination::Style || destination.is_script_like() => true, - // Step 8 + // Step 6 _ => false, } } diff --git a/components/net/lib.rs b/components/net/lib.rs index b73965f03d5..1c9c987a691 100644 --- a/components/net/lib.rs +++ b/components/net/lib.rs @@ -33,9 +33,11 @@ pub mod resource_thread; mod storage_thread; pub mod subresource_integrity; mod websocket_loader; + /// An implementation of the [Fetch specification](https://fetch.spec.whatwg.org/) pub mod fetch { pub mod cors_cache; + pub mod headers; pub mod methods; } diff --git a/tests/wpt/metadata/fetch/nosniff/parsing-nosniff.window.js.ini b/tests/wpt/metadata/fetch/nosniff/parsing-nosniff.window.js.ini index 30e1b851fd4..329a56c9c51 100644 --- a/tests/wpt/metadata/fetch/nosniff/parsing-nosniff.window.js.ini +++ b/tests/wpt/metadata/fetch/nosniff/parsing-nosniff.window.js.ini @@ -8,6 +8,3 @@ [X-Content-Type-Options%3A%20nosniff%0C] expected: FAIL - [X-Content-Type-Options%3A%20nosniff%2C%2C%40%23%24%23%25%25%26%5E%26%5E*()()11!] - expected: FAIL -